From 277ad98011e5da91d28891f751a8e8a154071f0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 07:56:40 +0000 Subject: [PATCH 1/9] Initial plan From acf211062fca6eb9a123e97746f69af83f0764a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 08:02:45 +0000 Subject: [PATCH 2/9] Respect doing_cron transient in wp cron event run --due-now Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/cron-event.feature | 32 ++++++++++++++++++++++++++++++++ src/Cron_Event_Command.php | 21 ++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/features/cron-event.feature b/features/cron-event.feature index 19186500..629efaa0 100644 --- a/features/cron-event.feature +++ b/features/cron-event.feature @@ -244,3 +244,35 @@ Feature: Manage WP Cron events """ Debug: Beginning execution of cron event 'wp_version_check' """ + + Scenario: --due-now respects the doing_cron transient and skips when another run is in progress + When I run `wp cron event schedule wp_cli_test_event_lock now hourly` + Then STDOUT should contain: + """ + Success: Scheduled event with hook 'wp_cli_test_event_lock' + """ + + # Simulate an in-progress cron run by setting the doing_cron transient to now. + When I run `wp eval 'set_transient( "doing_cron", sprintf( "%.22F", microtime( true ) ) );'` + + When I run `wp cron event run --due-now` + Then STDERR should contain: + """ + Warning: A cron event run is already in progress; skipping. + """ + And STDOUT should not contain: + """ + wp_cli_test_event_lock + """ + + # After the transient is cleared, the run should proceed normally. + When I run `wp transient delete doing_cron` + And I run `wp cron event run --due-now` + Then STDOUT should contain: + """ + Executed the cron event 'wp_cli_test_event_lock' + """ + And STDOUT should contain: + """ + Executed a total of 1 cron event + """ diff --git a/src/Cron_Event_Command.php b/src/Cron_Event_Command.php index 263deb64..362d3f0e 100644 --- a/src/Cron_Event_Command.php +++ b/src/Cron_Event_Command.php @@ -226,7 +226,8 @@ public function schedule( $args, $assoc_args ) { * : One or more hooks to run. * * [--due-now] - * : Run all hooks due right now. + * : Run all hooks due right now. Respects the doing_cron transient to + * prevent overlapping runs. * * [--exclude=] * : Comma-separated list of hooks to exclude. @@ -243,10 +244,24 @@ public function schedule( $args, $assoc_args ) { * Success: Executed a total of 2 cron events. */ public function run( $args, $assoc_args ) { + $due_now = Utils\get_flag_value( $assoc_args, 'due-now' ); + + if ( $due_now ) { + $lock_timeout = defined( 'WP_CRON_LOCK_TIMEOUT' ) ? WP_CRON_LOCK_TIMEOUT : 60; + $doing_cron_transient = get_transient( 'doing_cron' ); + if ( is_string( $doing_cron_transient ) && (float) $doing_cron_transient > microtime( true ) - $lock_timeout ) { + WP_CLI::warning( 'A cron event run is already in progress; skipping.' ); + return; + } + set_transient( 'doing_cron', sprintf( '%.22F', microtime( true ) ) ); + } $events = self::get_selected_cron_events( $args, $assoc_args ); if ( is_wp_error( $events ) ) { + if ( $due_now ) { + delete_transient( 'doing_cron' ); + } WP_CLI::error( $events ); } @@ -263,6 +278,10 @@ public function run( $args, $assoc_args ) { } } + if ( $due_now ) { + delete_transient( 'doing_cron' ); + } + $message = ( 1 === $executed ) ? 'Executed a total of %d cron event.' : 'Executed a total of %d cron events.'; WP_CLI::success( sprintf( $message, $executed ) ); } From 56dda5082c96a2804e3530ccb7d85735f4eb8421 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 15 Mar 2026 09:30:10 +0100 Subject: [PATCH 3/9] Apply suggestions from code review Co-authored-by: Pascal Birchler --- features/cron-event.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/cron-event.feature b/features/cron-event.feature index 629efaa0..fd580d4a 100644 --- a/features/cron-event.feature +++ b/features/cron-event.feature @@ -255,7 +255,7 @@ Feature: Manage WP Cron events # Simulate an in-progress cron run by setting the doing_cron transient to now. When I run `wp eval 'set_transient( "doing_cron", sprintf( "%.22F", microtime( true ) ) );'` - When I run `wp cron event run --due-now` + And I try `wp cron event run --due-now` Then STDERR should contain: """ Warning: A cron event run is already in progress; skipping. @@ -267,7 +267,7 @@ Feature: Manage WP Cron events # After the transient is cleared, the run should proceed normally. When I run `wp transient delete doing_cron` - And I run `wp cron event run --due-now` + And I try `wp cron event run --due-now` Then STDOUT should contain: """ Executed the cron event 'wp_cli_test_event_lock' From 59b9a4e838fedf7bb44478e0ba945fc95626792f Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 16 Mar 2026 10:52:09 +0100 Subject: [PATCH 4/9] Fix test --- features/cron-event.feature | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/features/cron-event.feature b/features/cron-event.feature index fd580d4a..a1010732 100644 --- a/features/cron-event.feature +++ b/features/cron-event.feature @@ -266,13 +266,9 @@ Feature: Manage WP Cron events """ # After the transient is cleared, the run should proceed normally. - When I run `wp transient delete doing_cron` + When I run `wp eval 'delete_transient( "doing_cron" );'` And I try `wp cron event run --due-now` Then STDOUT should contain: """ Executed the cron event 'wp_cli_test_event_lock' """ - And STDOUT should contain: - """ - Executed a total of 1 cron event - """ From 1d156d4d4d866ae5675060cdc346f95259ea2978 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:14:00 +0000 Subject: [PATCH 5/9] Use wpcli_ prefix to distinguish WP-CLI cron lock from spawn_cron transient Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/cron-event.feature | 5 +++-- src/Cron_Event_Command.php | 12 ++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/features/cron-event.feature b/features/cron-event.feature index a1010732..09247591 100644 --- a/features/cron-event.feature +++ b/features/cron-event.feature @@ -252,8 +252,9 @@ Feature: Manage WP Cron events Success: Scheduled event with hook 'wp_cli_test_event_lock' """ - # Simulate an in-progress cron run by setting the doing_cron transient to now. - When I run `wp eval 'set_transient( "doing_cron", sprintf( "%.22F", microtime( true ) ) );'` + # Simulate an in-progress WP-CLI cron run by setting the doing_cron transient + # with the 'wpcli_' prefix that WP-CLI uses. + When I run `wp eval 'set_transient( "doing_cron", "wpcli_" . sprintf( "%.22F", microtime( true ) ) );'` And I try `wp cron event run --due-now` Then STDERR should contain: diff --git a/src/Cron_Event_Command.php b/src/Cron_Event_Command.php index 362d3f0e..56e9f50d 100644 --- a/src/Cron_Event_Command.php +++ b/src/Cron_Event_Command.php @@ -249,11 +249,19 @@ public function run( $args, $assoc_args ) { if ( $due_now ) { $lock_timeout = defined( 'WP_CRON_LOCK_TIMEOUT' ) ? WP_CRON_LOCK_TIMEOUT : 60; $doing_cron_transient = get_transient( 'doing_cron' ); - if ( is_string( $doing_cron_transient ) && (float) $doing_cron_transient > microtime( true ) - $lock_timeout ) { + // Only treat the lock as belonging to another WP-CLI run when WP-CLI + // itself set it (value starts with 'wpcli_'). WordPress's spawn_cron() + // always stores a plain numeric microtime string, so we must not block + // ourselves on a transient that was set by spawn_cron during bootstrap. + if ( + is_string( $doing_cron_transient ) && + 0 === strncmp( $doing_cron_transient, 'wpcli_', 6 ) && + (float) substr( $doing_cron_transient, 6 ) > microtime( true ) - $lock_timeout + ) { WP_CLI::warning( 'A cron event run is already in progress; skipping.' ); return; } - set_transient( 'doing_cron', sprintf( '%.22F', microtime( true ) ) ); + set_transient( 'doing_cron', 'wpcli_' . sprintf( '%.22F', microtime( true ) ) ); } $events = self::get_selected_cron_events( $args, $assoc_args ); From d795834cdac1cf10c809a25444f7091519d9f0a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:42:00 +0000 Subject: [PATCH 6/9] Use wp_cli_doing_cron transient instead of value prefix trick Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/cron-event.feature | 7 +++---- src/Cron_Event_Command.php | 18 +++++------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/features/cron-event.feature b/features/cron-event.feature index 09247591..dbf9d98f 100644 --- a/features/cron-event.feature +++ b/features/cron-event.feature @@ -252,9 +252,8 @@ Feature: Manage WP Cron events Success: Scheduled event with hook 'wp_cli_test_event_lock' """ - # Simulate an in-progress WP-CLI cron run by setting the doing_cron transient - # with the 'wpcli_' prefix that WP-CLI uses. - When I run `wp eval 'set_transient( "doing_cron", "wpcli_" . sprintf( "%.22F", microtime( true ) ) );'` + # Simulate an in-progress WP-CLI cron run by setting the wp_cli_doing_cron transient. + When I run `wp eval 'set_transient( "wp_cli_doing_cron", sprintf( "%.22F", microtime( true ) ) );'` And I try `wp cron event run --due-now` Then STDERR should contain: @@ -267,7 +266,7 @@ Feature: Manage WP Cron events """ # After the transient is cleared, the run should proceed normally. - When I run `wp eval 'delete_transient( "doing_cron" );'` + When I run `wp eval 'delete_transient( "wp_cli_doing_cron" );'` And I try `wp cron event run --due-now` Then STDOUT should contain: """ diff --git a/src/Cron_Event_Command.php b/src/Cron_Event_Command.php index 56e9f50d..cba1bd4c 100644 --- a/src/Cron_Event_Command.php +++ b/src/Cron_Event_Command.php @@ -248,27 +248,19 @@ public function run( $args, $assoc_args ) { if ( $due_now ) { $lock_timeout = defined( 'WP_CRON_LOCK_TIMEOUT' ) ? WP_CRON_LOCK_TIMEOUT : 60; - $doing_cron_transient = get_transient( 'doing_cron' ); - // Only treat the lock as belonging to another WP-CLI run when WP-CLI - // itself set it (value starts with 'wpcli_'). WordPress's spawn_cron() - // always stores a plain numeric microtime string, so we must not block - // ourselves on a transient that was set by spawn_cron during bootstrap. - if ( - is_string( $doing_cron_transient ) && - 0 === strncmp( $doing_cron_transient, 'wpcli_', 6 ) && - (float) substr( $doing_cron_transient, 6 ) > microtime( true ) - $lock_timeout - ) { + $doing_cron_transient = get_transient( 'wp_cli_doing_cron' ); + if ( is_numeric( $doing_cron_transient ) && (float) $doing_cron_transient > microtime( true ) - $lock_timeout ) { WP_CLI::warning( 'A cron event run is already in progress; skipping.' ); return; } - set_transient( 'doing_cron', 'wpcli_' . sprintf( '%.22F', microtime( true ) ) ); + set_transient( 'wp_cli_doing_cron', sprintf( '%.22F', microtime( true ) ) ); } $events = self::get_selected_cron_events( $args, $assoc_args ); if ( is_wp_error( $events ) ) { if ( $due_now ) { - delete_transient( 'doing_cron' ); + delete_transient( 'wp_cli_doing_cron' ); } WP_CLI::error( $events ); } @@ -287,7 +279,7 @@ public function run( $args, $assoc_args ) { } if ( $due_now ) { - delete_transient( 'doing_cron' ); + delete_transient( 'wp_cli_doing_cron' ); } $message = ( 1 === $executed ) ? 'Executed a total of %d cron event.' : 'Executed a total of %d cron events.'; From d558edac2f63e0384a4d182c495c1dd163c3148a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:59:49 +0000 Subject: [PATCH 7/9] Use standard doing_cron transient with plain numeric microtime value Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/cron-event.feature | 6 +++--- src/Cron_Event_Command.php | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/features/cron-event.feature b/features/cron-event.feature index dbf9d98f..648a71f4 100644 --- a/features/cron-event.feature +++ b/features/cron-event.feature @@ -252,8 +252,8 @@ Feature: Manage WP Cron events Success: Scheduled event with hook 'wp_cli_test_event_lock' """ - # Simulate an in-progress WP-CLI cron run by setting the wp_cli_doing_cron transient. - When I run `wp eval 'set_transient( "wp_cli_doing_cron", sprintf( "%.22F", microtime( true ) ) );'` + # Simulate an in-progress cron run by setting the doing_cron transient. + When I run `wp eval 'set_transient( "doing_cron", sprintf( "%.22F", microtime( true ) ) );'` And I try `wp cron event run --due-now` Then STDERR should contain: @@ -266,7 +266,7 @@ Feature: Manage WP Cron events """ # After the transient is cleared, the run should proceed normally. - When I run `wp eval 'delete_transient( "wp_cli_doing_cron" );'` + When I run `wp eval 'delete_transient( "doing_cron" );'` And I try `wp cron event run --due-now` Then STDOUT should contain: """ diff --git a/src/Cron_Event_Command.php b/src/Cron_Event_Command.php index cba1bd4c..3f28e6f5 100644 --- a/src/Cron_Event_Command.php +++ b/src/Cron_Event_Command.php @@ -248,19 +248,19 @@ public function run( $args, $assoc_args ) { if ( $due_now ) { $lock_timeout = defined( 'WP_CRON_LOCK_TIMEOUT' ) ? WP_CRON_LOCK_TIMEOUT : 60; - $doing_cron_transient = get_transient( 'wp_cli_doing_cron' ); + $doing_cron_transient = get_transient( 'doing_cron' ); if ( is_numeric( $doing_cron_transient ) && (float) $doing_cron_transient > microtime( true ) - $lock_timeout ) { WP_CLI::warning( 'A cron event run is already in progress; skipping.' ); return; } - set_transient( 'wp_cli_doing_cron', sprintf( '%.22F', microtime( true ) ) ); + set_transient( 'doing_cron', sprintf( '%.22F', microtime( true ) ) ); } $events = self::get_selected_cron_events( $args, $assoc_args ); if ( is_wp_error( $events ) ) { if ( $due_now ) { - delete_transient( 'wp_cli_doing_cron' ); + delete_transient( 'doing_cron' ); } WP_CLI::error( $events ); } @@ -279,7 +279,7 @@ public function run( $args, $assoc_args ) { } if ( $due_now ) { - delete_transient( 'wp_cli_doing_cron' ); + delete_transient( 'doing_cron' ); } $message = ( 1 === $executed ) ? 'Executed a total of %d cron event.' : 'Executed a total of %d cron events.'; From 4a8c806592cc2485a8c74bd9116d7f0e5583c4fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:28:44 +0000 Subject: [PATCH 8/9] Fix cron.feature:309 test by disabling spawn_cron() interference Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/cron.feature | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/features/cron.feature b/features/cron.feature index 946b3c5e..7656a1c5 100644 --- a/features/cron.feature +++ b/features/cron.feature @@ -307,8 +307,13 @@ Feature: Manage WP-Cron events and schedules # executes the "wp_privacy_delete_old_export_files" event there. @require-wp-5.0 Scenario: Run currently scheduled events + # Disable web-triggered cron so spawn_cron() cannot set doing_cron between + # steps - wp cron event run always defines DOING_CRON so this has no effect + # on the commands being tested. + When I run `wp config set DISABLE_WP_CRON true --raw` + # WP throws a notice here for older versions of core. - When I try `wp cron event run --all` + And I try `wp cron event run --all` Then STDOUT should contain: """ Executed the cron event 'wp_version_check' From 1cf11638cf415f3ed8662f036f0533e44ed4f63d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:51:39 +0000 Subject: [PATCH 9/9] Set TTL on doing_cron transient; only delete if lock value still matches Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Cron_Event_Command.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Cron_Event_Command.php b/src/Cron_Event_Command.php index 3f28e6f5..ca56e3a7 100644 --- a/src/Cron_Event_Command.php +++ b/src/Cron_Event_Command.php @@ -246,6 +246,8 @@ public function schedule( $args, $assoc_args ) { public function run( $args, $assoc_args ) { $due_now = Utils\get_flag_value( $assoc_args, 'due-now' ); + $doing_cron_value = null; + if ( $due_now ) { $lock_timeout = defined( 'WP_CRON_LOCK_TIMEOUT' ) ? WP_CRON_LOCK_TIMEOUT : 60; $doing_cron_transient = get_transient( 'doing_cron' ); @@ -253,13 +255,14 @@ public function run( $args, $assoc_args ) { WP_CLI::warning( 'A cron event run is already in progress; skipping.' ); return; } - set_transient( 'doing_cron', sprintf( '%.22F', microtime( true ) ) ); + $doing_cron_value = sprintf( '%.22F', microtime( true ) ); + set_transient( 'doing_cron', $doing_cron_value, $lock_timeout ); } $events = self::get_selected_cron_events( $args, $assoc_args ); if ( is_wp_error( $events ) ) { - if ( $due_now ) { + if ( $due_now && get_transient( 'doing_cron' ) === $doing_cron_value ) { delete_transient( 'doing_cron' ); } WP_CLI::error( $events ); @@ -278,7 +281,7 @@ public function run( $args, $assoc_args ) { } } - if ( $due_now ) { + if ( $due_now && get_transient( 'doing_cron' ) === $doing_cron_value ) { delete_transient( 'doing_cron' ); }