From 0f48756c6aeb9e089c0b5516bbfa644658d0337a Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Tue, 17 Mar 2026 00:19:48 +0000 Subject: [PATCH] fix: prevent schedule reset when scheduling config hasn't changed handle_scheduling_update() unconditionally unscheduled and rescheduled flows even when the incoming config was identical to what was already set. This reset the Action Scheduler timer and triggered an immediate run every time any flow update included scheduling_config. Adds scheduling_unchanged() guard that compares incoming interval, cron_expression, or timestamp against the current DB values and returns early if nothing changed. Covers recurring, cron, one_time, and manual scheduling types. --- inc/Api/Flows/FlowScheduling.php | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/inc/Api/Flows/FlowScheduling.php b/inc/Api/Flows/FlowScheduling.php index 471fe7d0..ea760c32 100644 --- a/inc/Api/Flows/FlowScheduling.php +++ b/inc/Api/Flows/FlowScheduling.php @@ -45,6 +45,55 @@ public static function calculate_stagger_offset( int $flow_id, int $interval_sec return absint( crc32( 'dm_stagger_' . $flow_id ) ) % $max_offset; } + /** + * Check if the incoming scheduling config matches what's already set. + * + * Compares the meaningful scheduling fields (interval, cron_expression, + * timestamp) to determine if rescheduling would be a no-op. This prevents + * flow updates from resetting the Action Scheduler timer when the schedule + * hasn't actually changed. + * + * @param array $current Current scheduling_config from DB. + * @param string|null $interval Incoming interval key. + * @param string|null $cron_expression Incoming cron expression. + * @param array $incoming Full incoming scheduling_config. + * @return bool True if scheduling hasn't changed and can be skipped. + */ + private static function scheduling_unchanged( array $current, ?string $interval, ?string $cron_expression, array $incoming ): bool { + $current_interval = $current['interval'] ?? null; + + // If current is empty/unset and incoming is non-manual, it's a change. + if ( empty( $current_interval ) && null !== $interval && 'manual' !== $interval ) { + return false; + } + + // Both manual — no change. + if ( ( 'manual' === $current_interval || null === $current_interval ) + && ( 'manual' === $interval || null === $interval ) ) { + return true; + } + + // Recurring interval comparison. + if ( $current_interval === $interval && 'cron' !== $interval && 'one_time' !== $interval ) { + return true; + } + + // Cron expression comparison. + if ( 'cron' === $current_interval && 'cron' === $interval ) { + $current_cron = $current['cron_expression'] ?? ''; + return $current_cron === $cron_expression; + } + + // One-time timestamp comparison. + if ( 'one_time' === $current_interval && 'one_time' === $interval ) { + $current_ts = $current['timestamp'] ?? null; + $incoming_ts = $incoming['timestamp'] ?? null; + return $current_ts === $incoming_ts; + } + + return false; + } + /** * Validate a cron expression string. * @@ -168,6 +217,18 @@ public static function handle_scheduling_update( $flow_id, $scheduling_config ) $interval = $scheduling_config['interval'] ?? null; $cron_expression = $scheduling_config['cron_expression'] ?? null; + // Skip re-scheduling if the configuration hasn't actually changed. + // Without this guard, any flow update that includes scheduling_config + // (even identical to what's already set) would unschedule/reschedule, + // resetting the timer and triggering an immediate run. + $current_scheduling = $flow['scheduling_config'] ?? array(); + if ( is_string( $current_scheduling ) ) { + $current_scheduling = json_decode( $current_scheduling, true ) ?? array(); + } + if ( self::scheduling_unchanged( $current_scheduling, $interval, $cron_expression, $scheduling_config ) ) { + return true; + } + // Handle manual scheduling (unschedule). if ( 'manual' === $interval || null === $interval ) { if ( function_exists( 'as_unschedule_all_actions' ) ) {