From c4fe938e8fd3a7bf349a351f8020178b471c09aa Mon Sep 17 00:00:00 2001 From: Austin Gilmour Date: Fri, 15 May 2026 08:45:42 -0400 Subject: [PATCH 1/2] Use wp_schedule_single_event() for Run Now to fix Cavalcade compatibility force_schedule_single_event() bypassed wp_schedule_single_event() to avoid the duplicate-event check, but this meant third-party cron runners like Cavalcade never saw the pre_schedule_event filter fire. The result was a false "Failed to schedule" error even though the event was created in Cavalcade's database. Since the plugin requires WP 6.4+, the modern duplicate check uses abs($existing - $new_timestamp) <= 10 minutes. Scheduling at timestamp 1 (epoch) means this distance is always enormous for any real event, so the duplicate check never triggers. wp_schedule_single_event() is now safe to use directly, which allows Cavalcade and other cron runners to intercept the scheduling via their hooks. The force_schedule_single_event() function is removed as it's no longer used and was always an internal implementation detail. Fixes #56 --- src/event.php | 49 +++++-------------------------------------------- 1 file changed, 5 insertions(+), 44 deletions(-) diff --git a/src/event.php b/src/event.php index 7b4f90e..daf8d77 100644 --- a/src/event.php +++ b/src/event.php @@ -31,10 +31,13 @@ function run( $hookname, $sig ) { $event = Event::create_immediate( $hookname, $data['args'] ); delete_transient( 'doing_cron' ); - $scheduled = force_schedule_single_event( $hookname, $event->args ); // UTC + $scheduled = wp_schedule_single_event( 1, $hookname, $event->args, true ); // UTC if ( is_wp_error( $scheduled ) ) { - return $scheduled; + // A duplicate at timestamp 1 means it's already queued to run immediately — treat as success. + if ( 'duplicate_event' !== $scheduled->get_error_code() ) { + return $scheduled; + } } add_filter( 'cron_request', function ( array $cron_request_array ) { @@ -67,48 +70,6 @@ function run( $hookname, $sig ) { ); } -/** - * Forcibly schedules a single event for the purpose of manually running it. - * - * This is used instead of `wp_schedule_single_event()` to avoid the duplicate check that's otherwise performed. - * - * @param string $hook Action hook to execute when the event is run. - * @param mixed[] $args Optional. Array containing each separate argument to pass to the hook's callback function. - * @return true|WP_Error True if event successfully scheduled. WP_Error on failure. - */ -function force_schedule_single_event( $hook, $args = array() ) { - $event = (object) array( - 'hook' => $hook, - 'timestamp' => 1, - 'schedule' => false, - 'args' => $args, - ); - $crons = get_core_cron_array(); - $key = md5( serialize( $event->args ) ); - - $crons[ $event->timestamp ][ $event->hook ][ $key ] = array( - 'schedule' => $event->schedule, - 'args' => $event->args, - ); - ksort( $crons ); - - $result = _set_cron_array( $crons ); - - // Not using the WP_Error from `_set_cron_array()` here so we can provide a more specific error message. - if ( false === $result ) { - return new WP_Error( - 'could_not_add', - sprintf( - /* translators: %s: The name of the cron event. */ - __( 'Failed to schedule the cron event %s.', 'wp-crontrol' ), - $hook - ) - ); - } - - return true; -} - /** * Adds a new cron event. * From e0b363cf708f2618b0a3cf8865711194ef196184 Mon Sep 17 00:00:00 2001 From: Austin Gilmour Date: Fri, 12 Jun 2026 08:41:15 -0400 Subject: [PATCH 2/2] Handle Cavalcade false-positive duplicate_event on Run Now Cavalcade's pre_schedule_event uses a 10-minute window centred on the new timestamp. For timestamp=1 (epoch), that window is [0, now+600], which catches any event due within the next 10 minutes and returns duplicate_event without scheduling anything at timestamp=1. On duplicate_event, check wp_next_scheduled(): if the result is not 1, the error was a false positive from the original near-future event. Unschedule it and retry wp_schedule_single_event(1, ...) so Cavalcade creates the job at timestamp=1 as intended. Co-Authored-By: Claude Sonnet 4.6 --- src/event.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/event.php b/src/event.php index daf8d77..b70dee0 100644 --- a/src/event.php +++ b/src/event.php @@ -34,10 +34,23 @@ function run( $hookname, $sig ) { $scheduled = wp_schedule_single_event( 1, $hookname, $event->args, true ); // UTC if ( is_wp_error( $scheduled ) ) { - // A duplicate at timestamp 1 means it's already queued to run immediately — treat as success. if ( 'duplicate_event' !== $scheduled->get_error_code() ) { return $scheduled; } + + // A duplicate_event error can be a false positive: Cavalcade's pre_schedule_event + // uses a 10-minute window centred on timestamp=1, so it flags the original near-future + // event as a duplicate without actually scheduling anything at timestamp=1. + // If no job exists at timestamp=1, unschedule the future one and retry. + $next = wp_next_scheduled( $hookname, $event->args ); + if ( false !== $next && 1 !== $next ) { + wp_unschedule_event( $next, $hookname, $event->args ); + $scheduled = wp_schedule_single_event( 1, $hookname, $event->args, true ); + if ( is_wp_error( $scheduled ) ) { + return $scheduled; + } + } + // else: a job at timestamp=1 already exists, already queued to run immediately. } add_filter( 'cron_request', function ( array $cron_request_array ) {