diff --git a/inc/class-cron.php b/inc/class-cron.php index 3f8daa53..d6d63497 100644 --- a/inc/class-cron.php +++ b/inc/class-cron.php @@ -80,26 +80,45 @@ public function create_schedules(): void { /* * Hourly check */ - if ($this->has_recurring_hook_callback('wu_hourly') && wu_next_scheduled_action('wu_hourly') === false) { - $next_hour = strtotime(gmdate('Y-m-d H:00:00', strtotime('+1 hour'))); - - wu_schedule_recurring_action($next_hour, HOUR_IN_SECONDS, 'wu_hourly', [], 'wu_cron'); - } + $next_hour = strtotime(gmdate('Y-m-d H:00:00', strtotime('+1 hour'))); + $this->schedule_recurring_hook_when_needed('wu_hourly', $next_hour, HOUR_IN_SECONDS); /* * Daily check */ - if ($this->has_recurring_hook_callback('wu_daily') && wu_next_scheduled_action('wu_daily') === false) { - wu_schedule_recurring_action(strtotime('tomorrow'), DAY_IN_SECONDS, 'wu_daily', [], 'wu_cron'); - } + $this->schedule_recurring_hook_when_needed('wu_daily', strtotime('tomorrow'), DAY_IN_SECONDS); /* * Monthly check */ - if ($this->has_recurring_hook_callback('wu_monthly') && wu_next_scheduled_action('wu_monthly') === false) { - $next_month = strtotime(gmdate('Y-m-01 00:00:00', strtotime('+1 month'))); + $next_month = strtotime(gmdate('Y-m-01 00:00:00', strtotime('+1 month'))); + $this->schedule_recurring_hook_when_needed('wu_monthly', $next_month, MONTH_IN_SECONDS); + } + + /** + * Schedule recurring hooks only when they have callbacks. + * + * Existing no-callback recurring actions are cleaned up so Action Scheduler + * does not keep retrying rows that cannot run any work. + * + * @since 2.5.2 + * @param string $hook Action Scheduler hook name. + * @param int $timestamp First run timestamp. + * @param int $interval Interval in seconds. + * @return void + */ + private function schedule_recurring_hook_when_needed(string $hook, int $timestamp, int $interval): void { + + if (false === $this->has_recurring_hook_callback($hook)) { + if (false !== wu_next_scheduled_action($hook, [], 'wu_cron')) { + wu_unschedule_all_actions($hook, [], 'wu_cron'); + } + + return; + } - wu_schedule_recurring_action($next_month, MONTH_IN_SECONDS, 'wu_monthly', [], 'wu_cron'); + if (false === wu_next_scheduled_action($hook, [], 'wu_cron')) { + wu_schedule_recurring_action($timestamp, $interval, $hook, [], 'wu_cron'); } } @@ -117,7 +136,7 @@ public function create_schedules(): void { */ private function has_recurring_hook_callback(string $hook): bool { - return has_action($hook) !== false; + return false !== has_action($hook); } /** diff --git a/inc/class-tracker.php b/inc/class-tracker.php index 862b3810..ad37f42d 100644 --- a/inc/class-tracker.php +++ b/inc/class-tracker.php @@ -84,11 +84,11 @@ public function init(): void { */ public function create_weekly_schedule(): void { - if (has_action('wu_weekly') === false) { + if (false === has_action('wu_weekly')) { return; } - if (wu_next_scheduled_action('wu_weekly') === false) { + if (false === wu_next_scheduled_action('wu_weekly')) { $next_week = strtotime('next monday'); wu_schedule_recurring_action($next_week, WEEK_IN_SECONDS, 'wu_weekly', [], 'wu_cron'); diff --git a/tests/WP_Ultimo/Cron_Test.php b/tests/WP_Ultimo/Cron_Test.php index 24ddcabb..db4db10e 100644 --- a/tests/WP_Ultimo/Cron_Test.php +++ b/tests/WP_Ultimo/Cron_Test.php @@ -26,6 +26,18 @@ public function set_up(): void { parent::set_up(); $this->cron = Cron::get_instance(); + wu_unschedule_all_actions('wu_monthly', [], 'wu_cron'); + } + + /** + * Tear down test fixtures. + */ + public function tear_down(): void { + + remove_action('wu_monthly', [$this, 'dummy_cron_callback']); + wu_unschedule_all_actions('wu_monthly', [], 'wu_cron'); + + parent::tear_down(); } /** @@ -109,4 +121,38 @@ public function test_async_mark_membership_as_expired_invalid(): void { $this->assertTrue(true); } + + /** + * Test create_schedules removes stale no-callback recurring actions. + */ + public function test_create_schedules_unschedules_no_callback_recurring_action(): void { + + wu_schedule_recurring_action(time() + HOUR_IN_SECONDS, MONTH_IN_SECONDS, 'wu_monthly', [], 'wu_cron'); + + $this->assertNotFalse(wu_next_scheduled_action('wu_monthly', [], 'wu_cron')); + + $this->cron->create_schedules(); + + $this->assertFalse(wu_next_scheduled_action('wu_monthly', [], 'wu_cron')); + } + + /** + * Test create_schedules keeps scheduling hooks that have callbacks. + */ + public function test_create_schedules_schedules_when_callback_exists(): void { + + add_action('wu_monthly', [$this, 'dummy_cron_callback']); + + $this->cron->create_schedules(); + + $this->assertNotFalse(wu_next_scheduled_action('wu_monthly', [], 'wu_cron')); + } + + /** + * Dummy callback for recurring cron tests. + */ + public function dummy_cron_callback(): void { + + $this->assertTrue(true); + } }