From 836d9b01493c7a8c66666c12e076597eab06e4fd Mon Sep 17 00:00:00 2001 From: Shakeeb Sadikeen Date: Sun, 11 Oct 2026 20:52:26 +0530 Subject: [PATCH 01/14] Fix date format using month (m) instead of minutes (i) in created_at gmdate('Y-m-d H:m:s') wrote the month number into the minutes slot, so created_at was off by up to an hour. Use H:i:s. --- includes/RapidLoad_Job.php | 2 +- includes/RapidLoad_Job_Data.php | 4 ++-- includes/modules/optimizer/RapidLoad_Job_Optimization.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/includes/RapidLoad_Job.php b/includes/RapidLoad_Job.php index f8e7fe838..5390142ed 100644 --- a/includes/RapidLoad_Job.php +++ b/includes/RapidLoad_Job.php @@ -63,7 +63,7 @@ public function __construct($args = []) }else{ - $this->created_at = gmdate( "Y-m-d H:m:s", time() ); + $this->created_at = gmdate( "Y-m-d H:i:s", time() ); } } diff --git a/includes/RapidLoad_Job_Data.php b/includes/RapidLoad_Job_Data.php index e45ffd3bb..fc53e1184 100644 --- a/includes/RapidLoad_Job_Data.php +++ b/includes/RapidLoad_Job_Data.php @@ -48,7 +48,7 @@ public function __construct($job, $type) $this->attempts = 0; $this->hits = 0; $this->status = 'queued'; - $this->created_at = gmdate( "Y-m-d H:m:s", time() ); + $this->created_at = gmdate( "Y-m-d H:i:s", time() ); } @@ -120,7 +120,7 @@ public function requeue($attempts = -1){ $this->stats = null; $this->warnings = null; $this->error = null; - $this->created_at = gmdate( "Y-m-d H:m:s", time() ); + $this->created_at = gmdate( "Y-m-d H:i:s", time() ); $this->clearFiles(); } diff --git a/includes/modules/optimizer/RapidLoad_Job_Optimization.php b/includes/modules/optimizer/RapidLoad_Job_Optimization.php index 1ae696dc1..a2ce78620 100644 --- a/includes/modules/optimizer/RapidLoad_Job_Optimization.php +++ b/includes/modules/optimizer/RapidLoad_Job_Optimization.php @@ -28,7 +28,7 @@ public function __construct($job, $strategy, $id = null) $this->data = $exist->data; $this->created_at = $exist->created_at; }else{ - $this->created_at = gmdate( "Y-m-d H:m:s", time() ); + $this->created_at = gmdate( "Y-m-d H:i:s", time() ); } } From d5a33024e0aa5151b8443d68f4ce0525f7c173a0 Mon Sep 17 00:00:00 2001 From: Shakeeb Sadikeen Date: Sun, 11 Oct 2026 20:52:35 +0530 Subject: [PATCH 02/14] Do not reset job attempts on every successful page view mark_as_successful_hit() zeroed attempts on each view, which defeats any retry cap. Attempts are now reset only when the API actually returns a successful result (mark_as_success), which ends the retry cycle. --- includes/RapidLoad_Job_Data.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/RapidLoad_Job_Data.php b/includes/RapidLoad_Job_Data.php index fc53e1184..871933f07 100644 --- a/includes/RapidLoad_Job_Data.php +++ b/includes/RapidLoad_Job_Data.php @@ -144,13 +144,15 @@ public function mark_as_success($data, $stats, $warnings, $status = 'success', $ $this->status = isset($data) && (isset($data['mobile']) && !empty($data['mobile']) || isset($data['desktop']) && !empty($data['desktop'])) ? 'success' : 'failed'; } $this->hits = 0; + // A genuine API success ends the retry cycle. (Page views must not reset + // attempts, see mark_as_successful_hit, or the retry cap is defeated.) + $this->attempts = 0; $this->stats = isset($stats) ? serialize($stats) : null; $this->warnings = isset($warnings) && count($warnings) > 0 ? $warnings : null; $this->error = null; } public function mark_as_successful_hit(){ - $this->attempts = 0; $this->hits++; $this->error = NULL; } From c205be9741ad20b3a8bb5fefe9f7413dfd1caed3 Mon Sep 17 00:00:00 2001 From: Shakeeb Sadikeen Date: Sun, 11 Oct 2026 20:52:39 +0530 Subject: [PATCH 03/14] Make cleanCacheFiles consider all successful CPCSS rows get_success_data() defaulted to LIMIT 1, so cleanCacheFiles() treated only one URL's files as in use and deleted every other cached CPCSS file, which forced those URLs to be requeued on their next view. A limit of 0 now returns all rows; cleanCacheFiles passes 0. Default (1) is unchanged for other callers. The UnusedCSS cleanCacheFiles() is an empty stub, so it is not affected. --- .../css/critical-css/RapidLoad_CriticalCSS.php | 2 +- .../css/critical-css/RapidLoad_CriticalCSS_DB.php | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/includes/modules/css/critical-css/RapidLoad_CriticalCSS.php b/includes/modules/css/critical-css/RapidLoad_CriticalCSS.php index e0baec6ae..101a8dd2b 100644 --- a/includes/modules/css/critical-css/RapidLoad_CriticalCSS.php +++ b/includes/modules/css/critical-css/RapidLoad_CriticalCSS.php @@ -478,7 +478,7 @@ public function init_async_store($job_data, $args) public function cleanCacheFiles(){ - $data = RapidLoad_CriticalCSS_DB::get_success_data(); + $data = RapidLoad_CriticalCSS_DB::get_success_data(0); $used_files = []; diff --git a/includes/modules/css/critical-css/RapidLoad_CriticalCSS_DB.php b/includes/modules/css/critical-css/RapidLoad_CriticalCSS_DB.php index 3fd669c1a..8eb501fbb 100644 --- a/includes/modules/css/critical-css/RapidLoad_CriticalCSS_DB.php +++ b/includes/modules/css/critical-css/RapidLoad_CriticalCSS_DB.php @@ -140,12 +140,15 @@ public static function get_current_processing_tasks_job_ids($limit = 1){ public static function get_success_data($limit = 1) { global $wpdb; + // $limit = 0 (or negative) returns every successful row. + $limit = (int) $limit; + $limit_sql = $limit > 0 ? $wpdb->prepare( ' LIMIT %d', $limit ) : ''; + $data = $wpdb->get_results( $wpdb->prepare( - "SELECT data FROM {$wpdb->prefix}rapidload_job_data WHERE status = 'success' AND job_type = %s ORDER BY id DESC LIMIT %d", - 'cpcss', - $limit - ), + "SELECT data FROM {$wpdb->prefix}rapidload_job_data WHERE status = 'success' AND job_type = %s ORDER BY id DESC", + 'cpcss' + ) . $limit_sql, OBJECT ); From ffa5216717f6dc0f4899f762b6175bc16843b5f6 Mon Sep 17 00:00:00 2001 From: Shakeeb Sadikeen Date: Sun, 11 Oct 2026 20:53:22 +0530 Subject: [PATCH 04/14] Cap job retries and add exponential backoff when polling fails update_css() requeued forever on 'Job processing failed in queue'/'Error' with no cap and no delay, so a failing URL was resubmitted every queue tick. Add RapidLoad_Job_Data::retry_or_fail(): counts the attempt, requeues with exponential backoff (120s, 240s, ... max 1h, stored as a future created_at, no schema change) while attempts < max (default 3, filter rapidload/job/max_attempts or RAPIDLOAD_JOB_MAX_ATTEMPTS), then marks the job failed. The queued-job SELECTs skip retry rows (attempts >= 1) until created_at <= now. Rows with attempts = 0 are unaffected, and user/page-view requeues set created_at = now so they stay immediately eligible. --- includes/RapidLoad_Job_Data.php | 48 +++++++++++++++++++ .../critical-css/RapidLoad_CriticalCSS_DB.php | 3 +- .../RapidLoad_CriticalCSS_Store.php | 11 ++++- .../css/unused-css/RapidLoad_UnusedCSS_DB.php | 3 +- .../unused-css/RapidLoad_UnusedCSS_Store.php | 11 ++++- 5 files changed, 70 insertions(+), 6 deletions(-) diff --git a/includes/RapidLoad_Job_Data.php b/includes/RapidLoad_Job_Data.php index 871933f07..727fbfdea 100644 --- a/includes/RapidLoad_Job_Data.php +++ b/includes/RapidLoad_Job_Data.php @@ -124,6 +124,54 @@ public function requeue($attempts = -1){ $this->clearFiles(); } + /** + * Maximum number of API attempts for one job before it is marked as failed. + * Override with the RAPIDLOAD_JOB_MAX_ATTEMPTS constant or the + * 'rapidload/job/max_attempts' filter. + */ + public static function max_attempts(){ + $max = defined('RAPIDLOAD_JOB_MAX_ATTEMPTS') ? (int) RAPIDLOAD_JOB_MAX_ATTEMPTS : 3; + $max = (int) apply_filters('rapidload/job/max_attempts', $max); + return $max > 0 ? $max : 1; + } + + /** + * Exponential backoff (seconds) before a failed attempt may be retried: + * 120s, 240s, 480s ... capped at one hour. + */ + public static function retry_delay($attempts){ + $attempts = max(1, min((int) $attempts, 10)); + $delay = (int) apply_filters('rapidload/job/retry_base_delay', 120) * pow(2, $attempts - 1); + return min($delay, 3600); + } + + /** + * Counts one failed attempt. Requeues the job (not eligible for the queue + * until the backoff has elapsed) while attempts are left, otherwise marks + * it as failed with $error. + * + * The backoff is stored as a future created_at; the queue only picks up + * rows with attempts > 0 once created_at <= now (see the *_DB + * get_current_queued_tasks_job_ids methods). + * + * @return bool true when requeued, false when marked as failed + */ + public function retry_or_fail($error){ + + $attempts = (int) $this->attempts + 1; + + if($attempts >= self::max_attempts()){ + $this->mark_as_failed($error); + $this->attempts = $attempts; + return false; + } + + $this->requeue($attempts); + $this->created_at = gmdate( "Y-m-d H:i:s", time() + self::retry_delay($attempts) ); + + return true; + } + public function clearFiles(){ } diff --git a/includes/modules/css/critical-css/RapidLoad_CriticalCSS_DB.php b/includes/modules/css/critical-css/RapidLoad_CriticalCSS_DB.php index 8eb501fbb..7da5e540a 100644 --- a/includes/modules/css/critical-css/RapidLoad_CriticalCSS_DB.php +++ b/includes/modules/css/critical-css/RapidLoad_CriticalCSS_DB.php @@ -100,8 +100,9 @@ public static function get_current_queued_tasks_job_ids($limit = 1){ $job_ids = $wpdb->get_results( $wpdb->prepare( - "SELECT job_id FROM {$wpdb->prefix}rapidload_job_data WHERE status = 'queued' AND job_type = %s ORDER BY id DESC LIMIT %d", + "SELECT job_id FROM {$wpdb->prefix}rapidload_job_data WHERE status = 'queued' AND job_type = %s AND (attempts IS NULL OR attempts < 1 OR created_at <= %s) ORDER BY id DESC LIMIT %d", 'cpcss', + gmdate( 'Y-m-d H:i:s', time() ), $limit ), OBJECT diff --git a/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Store.php b/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Store.php index c0ae698d1..b94df9ee9 100644 --- a/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Store.php +++ b/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Store.php @@ -235,13 +235,20 @@ function update_css(){ if(isset($error['message']) && ($error['message'] === 'Job processing failed in queue' || $error['message'] === 'Error')){ + $requeued = $this->job_data->retry_or_fail($error); + self::log([ - 'log' => 'requeue-> cpcss job processing failed in queue', + 'log' => ($requeued ? 'requeue-> ' : 'giving up-> ') . 'cpcss job processing failed in queue (attempt ' . $this->job_data->attempts . ')', 'url' => $this->job_data->job->url, ]); - $this->job_data->requeue(); $this->job_data->save(); + if(!$requeued){ + do_action( 'uucss/cache_cleared', [ + 'url' => $this->job_data->job->url + ]); + } + return; } diff --git a/includes/modules/css/unused-css/RapidLoad_UnusedCSS_DB.php b/includes/modules/css/unused-css/RapidLoad_UnusedCSS_DB.php index fa7a0243e..4b053c17c 100644 --- a/includes/modules/css/unused-css/RapidLoad_UnusedCSS_DB.php +++ b/includes/modules/css/unused-css/RapidLoad_UnusedCSS_DB.php @@ -149,8 +149,9 @@ public static function get_current_queued_tasks_job_ids($limit = 1){ $job_ids = $wpdb->get_results( $wpdb->prepare( - "SELECT job_id FROM {$wpdb->prefix}rapidload_job_data WHERE status = 'queued' AND job_type = %s ORDER BY id DESC LIMIT %d", + "SELECT job_id FROM {$wpdb->prefix}rapidload_job_data WHERE status = 'queued' AND job_type = %s AND (attempts IS NULL OR attempts < 1 OR created_at <= %s) ORDER BY id DESC LIMIT %d", 'uucss', + gmdate( 'Y-m-d H:i:s', time() ), $limit ), OBJECT diff --git a/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Store.php b/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Store.php index a95e8a1b8..8bf57d46d 100644 --- a/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Store.php +++ b/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Store.php @@ -250,13 +250,20 @@ function update_css(){ if(isset($error['message']) && ($error['message'] === 'Job processing failed in queue' || $error['message'] === 'Error')){ + $requeued = $this->job_data->retry_or_fail($error); + self::log([ - 'log' => 'requeue-> uucss job processing failed in queue', + 'log' => ($requeued ? 'requeue-> ' : 'giving up-> ') . 'uucss job processing failed in queue (attempt ' . $this->job_data->attempts . ')', 'url' => $this->job_data->job->url, ]); - $this->job_data->requeue(); $this->job_data->save(); + if(!$requeued){ + do_action( 'uucss/cache_cleared', [ + 'url' => $this->job_data->job->url + ]); + } + return; } From 176fd3e59578f1ee8090888536a402981f7a458c Mon Sep 17 00:00:00 2001 From: Shakeeb Sadikeen Date: Sun, 11 Oct 2026 20:53:36 +0530 Subject: [PATCH 05/14] Treat purge responses without id or completed as an error A response with neither id nor completed (e.g. {"message":"rate limited"}) left the row 'queued' and it was resent on every queue tick. Now it counts an attempt via retry_or_fail() (backoff, then failed after the cap) for both CPCSS and UUCSS. Also guards against non-object results. --- .../RapidLoad_CriticalCSS_Store.php | 28 ++++++++++++++++++- .../unused-css/RapidLoad_UnusedCSS_Store.php | 28 ++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Store.php b/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Store.php index b94df9ee9..7dc62de1d 100644 --- a/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Store.php +++ b/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Store.php @@ -138,7 +138,7 @@ function purge_css(){ $this->job_data->status = 'waiting'; $this->job_data->save(); - }else if($result->completed){ + }else if(is_object($result) && !empty($result->completed)){ $this->result = $result; $this->purged_css = $result->data; @@ -147,6 +147,32 @@ function purge_css(){ $this->cache_file($this->purged_css, $this->purged_mobile_css); $this->cpcss_cached($this->job_data->job->url); + }else{ + + // Neither a queued job id nor a completed result: count the attempt + // instead of silently leaving the row 'queued' to be resent every tick. + $error = [ + 'code' => 500, + 'message' => is_object($result) && isset($result->message) && is_string($result->message) + ? $result->message + : 'Unexpected response from RapidLoad API' + ]; + + $requeued = $this->job_data->retry_or_fail($error); + $this->job_data->save(); + + $this->log( [ + 'log' => ($requeued ? 'requeue-> ' : 'giving up-> ') . 'unexpected response from api (attempt ' . $this->job_data->attempts . ')', + 'url' => $this->job_data->job->url, + 'type' => 'uucss-cron' + ] ); + + if(!$requeued){ + do_action( 'uucss/cache_cleared', [ + 'url' => $this->job_data->job->url + ]); + } + } } diff --git a/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Store.php b/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Store.php index 8bf57d46d..334435dac 100644 --- a/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Store.php +++ b/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Store.php @@ -120,7 +120,7 @@ public function purge_css() { $this->job_data->status = 'waiting'; $this->job_data->save(); - }else if($result->completed){ + }else if(is_object($result) && !empty($result->completed)){ $this->result = $result; $this->purged_css = $result->data; @@ -128,6 +128,32 @@ public function purge_css() { $this->cache_files($this->purged_css); $this->uucss_cached($this->job_data->job->url); + }else{ + + // Neither a queued job id nor a completed result: count the attempt + // instead of silently leaving the row 'queued' to be resent every tick. + $error = [ + 'code' => 500, + 'message' => is_object($result) && isset($result->message) && is_string($result->message) + ? $result->message + : 'Unexpected response from RapidLoad API' + ]; + + $requeued = $this->job_data->retry_or_fail($error); + $this->job_data->save(); + + $this->log( [ + 'log' => ($requeued ? 'requeue-> ' : 'giving up-> ') . 'unexpected response from api (attempt ' . $this->job_data->attempts . ')', + 'url' => $this->job_data->job->url, + 'type' => 'uucss-cron' + ] ); + + if(!$requeued){ + do_action( 'uucss/cache_cleared', [ + 'url' => $this->job_data->job->url + ]); + } + } } From ae232c7875f11afd626de5b3753180fb33363954 Mon Sep 17 00:00:00 2001 From: Shakeeb Sadikeen Date: Sun, 11 Oct 2026 20:54:55 +0530 Subject: [PATCH 06/14] Claim queued jobs atomically and lock the queue run The row only left 'queued' after the (up to 360s) API POST returned, so overlapping WP-Cron / rapidload_cron.php runs resent the same row. - RapidLoad_Job_Data::claim(): UPDATE ... SET status='submitting' WHERE id=%d AND status='queued'; the submitter skips the job if 0 rows changed. - RapidLoad_Job_Data::release_stale_claims(): a row stuck in 'submitting' for > 600s (run crashed/killed) counts as a failed attempt and goes back to 'queued' with backoff, or to 'failed' at the attempt cap. Called at the start of fetch_job_id for cpcss and uucss. - RapidLoad_Queue::run_task(): short-lived lock (INSERT IGNORE on an options row, expires after 600s so a crashed run cannot block forever) used by both the WP-Cron hook and rapidload_cron.php. --- includes/RapidLoad_Job_Data.php | 87 +++++++++++++++++ includes/RapidLoad_Queue.php | 93 ++++++++++++++++++- .../RapidLoad_CriticalCSS_Queue.php | 2 + .../RapidLoad_CriticalCSS_Store.php | 5 + .../unused-css/RapidLoad_UnusedCSS_Queue.php | 2 + .../unused-css/RapidLoad_UnusedCSS_Store.php | 5 + rapidload_cron.php | 8 +- 7 files changed, 200 insertions(+), 2 deletions(-) diff --git a/includes/RapidLoad_Job_Data.php b/includes/RapidLoad_Job_Data.php index 727fbfdea..fba93b33d 100644 --- a/includes/RapidLoad_Job_Data.php +++ b/includes/RapidLoad_Job_Data.php @@ -172,6 +172,93 @@ public function retry_or_fail($error){ return true; } + /** + * Seconds after which a claimed ('submitting') row is considered abandoned. + * Longer than the 360s API request timeout. + */ + const CLAIM_TIMEOUT = 600; + + /** + * Atomically moves this row out of 'queued' so only one queue run submits it. + * created_at is set to now and doubles as the claim time. + * + * @return bool true when this process won the claim + */ + public function claim(){ + + if(!isset($this->id)){ + return true; + } + + global $wpdb; + + $now = gmdate( "Y-m-d H:i:s", time() ); + + $affected = $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->prefix}rapidload_job_data SET status = 'submitting', created_at = %s WHERE id = %d AND status = 'queued'", + $now, + $this->id + ) + ); + + if($affected === 1){ + $this->status = 'submitting'; + $this->created_at = $now; + return true; + } + + return false; + } + + /** + * Recovers rows claimed by a run that died (fatal, timeout, killed) before + * the API answered. After CLAIM_TIMEOUT the claim counts as a failed attempt: + * the row returns to 'queued' (with the usual backoff) or, once the attempt + * cap is reached, is marked failed. A row can therefore never stay in + * 'submitting' forever, and a request that crashes PHP cannot loop forever. + */ + public static function release_stale_claims($job_type){ + + global $wpdb; + + $cutoff = gmdate( "Y-m-d H:i:s", time() - self::CLAIM_TIMEOUT ); + + $rows = $wpdb->get_results( + $wpdb->prepare( + "SELECT job_id FROM {$wpdb->prefix}rapidload_job_data WHERE status = 'submitting' AND job_type = %s AND created_at <= %s LIMIT 50", + $job_type, + $cutoff + ) + ); + + if(empty($rows)){ + return; + } + + foreach ($rows as $row){ + + $job = RapidLoad_Job::find_or_fail($row->job_id); + + if(!$job){ + continue; + } + + $job_data = new RapidLoad_Job_Data($job, $job_type); + + // re-check on the loaded object in case another run already recovered it + if($job_data->status !== 'submitting'){ + continue; + } + + $job_data->retry_or_fail([ + 'code' => 524, + 'message' => 'Request was interrupted before the API responded' + ]); + $job_data->save(); + } + } + public function clearFiles(){ } diff --git a/includes/RapidLoad_Queue.php b/includes/RapidLoad_Queue.php index 8d7d8e0f0..a36ac449c 100644 --- a/includes/RapidLoad_Queue.php +++ b/includes/RapidLoad_Queue.php @@ -102,10 +102,101 @@ static function get_post_types(){ function uucss_process_queue(){ - do_action('uucss/queue/task'); + self::run_task(); } + const LOCK_OPTION = 'rapidload_queue_lock'; + + /** + * A crashed run leaves its lock behind; it is considered dead after this + * many seconds (the claim of any row it held is recovered separately by + * RapidLoad_Job_Data::release_stale_claims). + */ + const LOCK_TTL = 600; + + /** + * Runs the queue task unless another run (WP-Cron or the system cron script) + * holds the lock. Shared by both entry points. + * + * @return bool false when skipped because another run is active + */ + public static function run_task(){ + + if(!self::acquire_lock()){ + return false; + } + + try{ + do_action('uucss/queue/task'); + }catch (Exception $e){ + self::release_lock(); + throw $e; + } + + self::release_lock(); + + return true; + } + + /** + * Atomic lock: INSERT IGNORE on the unique option_name succeeds for exactly + * one caller. An expired lock (crashed run) is taken over with a conditional + * DELETE so two callers cannot both steal it. + */ + public static function acquire_lock(){ + + global $wpdb; + + for ($i = 0; $i < 2; $i++) { + + $locked = $wpdb->query( + $wpdb->prepare( + "INSERT IGNORE INTO `$wpdb->options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, 'no')", + self::LOCK_OPTION, + (string) time() + ) + ); + + if($locked){ + return true; + } + + $current = $wpdb->get_var( + $wpdb->prepare( + "SELECT option_value FROM `$wpdb->options` WHERE option_name = %s", + self::LOCK_OPTION + ) + ); + + if($current !== null && (time() - (int) $current) < self::LOCK_TTL){ + return false; + } + + $wpdb->query( + $wpdb->prepare( + "DELETE FROM `$wpdb->options` WHERE option_name = %s AND option_value = %s", + self::LOCK_OPTION, + (string) $current + ) + ); + } + + return false; + } + + public static function release_lock(){ + + global $wpdb; + + $wpdb->query( + $wpdb->prepare( + "DELETE FROM `$wpdb->options` WHERE option_name = %s", + self::LOCK_OPTION + ) + ); + } + function uucss_process_queue_schedule($schedules){ $schedules['uucss_cron_interval'] = array( 'interval' => self::$interval, diff --git a/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Queue.php b/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Queue.php index 7187532fd..b03b419ea 100644 --- a/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Queue.php +++ b/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Queue.php @@ -17,6 +17,8 @@ public function __construct() function fetch_job_id(){ + RapidLoad_Job_Data::release_stale_claims('cpcss'); + $current_waiting = RapidLoad_CriticalCSS_DB::get_current_waiting_tasks_count(); if( (RapidLoad_Queue::$job_count - $current_waiting) <= 0 ){ diff --git a/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Store.php b/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Store.php index 7dc62de1d..2733bb716 100644 --- a/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Store.php +++ b/includes/modules/css/critical-css/RapidLoad_CriticalCSS_Store.php @@ -103,6 +103,11 @@ function purge_css(){ $cpcss_config = apply_filters('cpcss/purge/config', ( isset( $this->args['options'] ) ) ? $this->args['options'] : []); + if(!$this->job_data->claim()){ + // another queue run already took this job + return; + } + $result = $uucss_api->post( 's/criticalcss', array_merge( $cpcss_config, [ diff --git a/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Queue.php b/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Queue.php index 53f48dc11..3477faa25 100644 --- a/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Queue.php +++ b/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Queue.php @@ -18,6 +18,8 @@ public function __construct() function fetch_job_id(){ + RapidLoad_Job_Data::release_stale_claims('uucss'); + $current_waiting = RapidLoad_UnusedCSS_DB::get_current_waiting_tasks_count(); if( (RapidLoad_Queue::$job_count - $current_waiting) <= 0 ){ diff --git a/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Store.php b/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Store.php index 334435dac..c0c919a5f 100644 --- a/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Store.php +++ b/includes/modules/css/unused-css/RapidLoad_UnusedCSS_Store.php @@ -89,6 +89,11 @@ public function purge_css() { $uucss_config = apply_filters('uucss/purge/config', $api_options); + if(!$this->job_data->claim()){ + // another queue run already took this job + return; + } + $result = $uucss_api->post( 's/unusedcss', array_merge( $uucss_config, [ diff --git a/rapidload_cron.php b/rapidload_cron.php index 4f4b7315c..b8e0e788b 100644 --- a/rapidload_cron.php +++ b/rapidload_cron.php @@ -70,7 +70,13 @@ function rapidload_cron_log($message) { rapidload_cron_log('WordPress loaded from: ' . ABSPATH); // Trigger the optimization process - do_action('uucss/queue/task'); + if (class_exists('RapidLoad_Queue')) { + if (!RapidLoad_Queue::run_task()) { + rapidload_cron_log('Another RapidLoad queue run is active, skipping'); + } + } else { + do_action('uucss/queue/task'); + } // Log completion rapidload_cron_log('RapidLoad optimization process completed successfully'); From 32adf20c249939931be672bfaaf4b2affdabfee7 Mon Sep 17 00:00:00 2001 From: Shakeeb Sadikeen Date: Sun, 11 Oct 2026 21:01:06 +0530 Subject: [PATCH 07/14] Harden queue lock: unique token, refresh between jobs, release on shutdown - Lock value is "|