Add atomic cache-based mutex lock to prevent concurrent import cron execution#1198
Add atomic cache-based mutex lock to prevent concurrent import cron execution#1198
Conversation
…ution Co-authored-by: ineagu <1849868+ineagu@users.noreply.github.com> Agent-Logs-Url: https://github.com/Codeinwp/feedzy-rss-feeds/sessions/e881eaf6-2715-40de-8539-35fc0bd118cf
🌍 i18n String Review Report📊 Summary
➕ Added Strings (1) - Click to expand
|
There was a problem hiding this comment.
Pull request overview
Adds a per-import-job mutex in the import cron runner to reduce duplicate imports caused by overlapping WP-Cron executions.
Changes:
- Introduces a per-job transient lock key (
feedzy_import_lock_{job_id}) to skip already-running jobs. - Adds INFO logging when a job is skipped due to an existing lock.
- Ensures lock cleanup via a
finallyblock after each job run.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Set lock with 10-minute TTL | ||
| set_transient( $lock_key, time(), 10 * MINUTE_IN_SECONDS ); |
There was a problem hiding this comment.
The lock TTL is hard-coded to 10 minutes. This can expire while an import is still running (run_job() increases max execution time via the feedzy_max_execution_time filter), which would allow a second cron execution to acquire the lock and run concurrently. Suggest basing the TTL on the configured max execution time (plus a buffer) and/or making it filterable so longer-running imports don’t lose the lock mid-run.
| // Set lock with 10-minute TTL | |
| set_transient( $lock_key, time(), 10 * MINUTE_IN_SECONDS ); | |
| // Determine lock TTL based on max execution time (plus buffer) and allow filtering. | |
| $max_execution_time = (int) apply_filters( 'feedzy_max_execution_time', ini_get( 'max_execution_time' ) ); | |
| if ( $max_execution_time <= 0 ) { | |
| $max_execution_time = 300; // Fallback to 5 minutes if unlimited or not set. | |
| } | |
| $lock_ttl = (int) apply_filters( 'feedzy_import_lock_ttl', $max_execution_time + 60, $job ); | |
| set_transient( $lock_key, time(), $lock_ttl ); |
| 'error' => $e->getMessage(), | ||
| ) | ||
| ); | ||
| } finally { | ||
| // Always release the lock, even on error | ||
| delete_transient( $lock_key ); |
There was a problem hiding this comment.
This block still only catches Exception; PHP Errors/TypeErrors (Throwable) during imports would bypass the catch/logging and can abort cron processing. Consider switching the catch to Throwable (and logging it) to make cron execution more resilient and failures visible.
| // Check if job is already running | ||
| if ( get_transient( $lock_key ) ) { | ||
| Feedzy_Rss_Feeds_Log::info( | ||
| // translators: %1$s is the import job title, %2$d is the job ID. | ||
| sprintf( __( 'Skipping job "%1$s" (ID: %2$d) - already running', 'feedzy-rss-feeds' ), $job->post_title, $job->ID ), | ||
| array( | ||
| 'job_id' => $job->ID, | ||
| 'reason' => 'concurrent_execution_prevented', | ||
| ) | ||
| ); | ||
| continue; | ||
| } | ||
|
|
||
| // Set lock with 10-minute TTL | ||
| set_transient( $lock_key, time(), 10 * MINUTE_IN_SECONDS ); |
There was a problem hiding this comment.
The lock acquisition uses a non-atomic check-then-set (get_transient() followed by set_transient()). Two overlapping requests can both see no transient and then both set it, so the same job can still run concurrently (the exact race this lock is meant to prevent). Consider switching to an atomic primitive for acquisition (e.g., add_option() with a unique option name, or a cache add/"add"-semantics call) and only proceed when acquisition succeeds.
|
@claude[agent] can you fix the errors and avoid creating strings? |
…able, remove i18n string Co-authored-by: ineagu <1849868+ineagu@users.noreply.github.com> Agent-Logs-Url: https://github.com/Codeinwp/feedzy-rss-feeds/sessions/0a2662a5-9f58-44a8-ad51-bd8821fe85d6
Fixed in dc5c7d1:
|
WordPress pseudo-cron can fire the same scheduled event multiple times during overlapping page loads. Without synchronization, concurrent execution of the same import job causes duplicate posts (TOCTOU race in duplicate detection) and resource waste.
Changes
Added per-job mutex locking in
run_cron()atincludes/admin/feedzy-rss-feeds-import.php:1545-1608:Atomic lock acquisition
wp_cache_add()for atomic lock acquisition (returns false if lock already exists)feedzy_import_lockscache group with keyfeedzy_import_lock_{job_id}Dynamic lock TTL
feedzy_max_execution_timefilter (default 500s) + 60s bufferfeedzy_import_lock_ttlhook for customizationLock cleanup
finallyblock viawp_cache_delete()to guarantee cleanup on both success and error pathsError handling
Throwableinstead ofExceptionto handle PHP 8+ TypeError/ErrorLogging
Example
Behavior
finallyblockOriginal prompt
<issue_description>## Problem
WordPress pseudo-cron can fire the same scheduled event multiple times if page loads overlap. Without a lock, the same import job can run concurrently, causing duplicate posts (TOCTOU race condition in duplicate detection) and wasted server resources.
Solution
import.php — In
run_cron(), add a per-job transient lock:get_transient('feedzy_import_lock_' . $job->ID), skip if activefinally { delete_transient($lock_key); }to ensure cleanup on both success and errorFiles Affected
includes/admin/feedzy-rss-feeds-import.phpAcceptance Criteria
Priority: High — prevents duplicate imported posts
Regression risk: Low — worst case a job is skipped once, retries on next cron cycle</issue_description>
Comments on the Issue (you are @claude[agent] in this section)
Original prompt