Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
594871c
fix: keep the DB refresh scheduled when Action Scheduler creation fails
Alexia-Soare Sep 21, 2026
38786b1
fix: clear a WP-Cron event left beside the Action Scheduler action
Alexia-Soare Sep 21, 2026
22ccbba
test: skip instead of fatal when Action Scheduler is not loaded
Alexia-Soare Sep 21, 2026
afa7686
fix: recover the refresh chain a killed run ends, and throttle the check
Alexia-Soare Sep 21, 2026
c8cf512
fix: start a recovered run now, and stop rewriting an autoloaded option
Alexia-Soare Sep 21, 2026
3ea7339
test: prove the filter argument order instead of assuming it
Alexia-Soare Sep 21, 2026
d69c338
test: do not depend on plugin_basename() for the lifecycle hook name
Alexia-Soare Sep 21, 2026
29adb15
test: say why the Action Scheduler precondition fails instead of skip…
Alexia-Soare Sep 21, 2026
fb1f58d
fix: do not drop the refresh on an unknown interval or a fractional o…
Alexia-Soare Sep 21, 2026
091ffcf
docs: note that tear_down restores hooks, so test filters need no rem…
Alexia-Soare Sep 21, 2026
8b32e01
fix: record the check window only when a trigger exists
Alexia-Soare Sep 21, 2026
842a76c
fix: treat a live WP-Cron fallback as scheduled for the check window
Alexia-Soare Sep 22, 2026
f814207
refactor: read the cron schedules once when scheduling the refresh
Alexia-Soare Sep 22, 2026
dbd3176
fix: keep the old WP-Cron event until its replacement is scheduled
Alexia-Soare Sep 22, 2026
ede25f0
fix: unschedule the old refresh event without passing its arguments
Alexia-Soare Sep 22, 2026
da6fa8f
docs: trim the comments
Alexia-Soare Sep 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 117 additions & 35 deletions classes/Visualizer/Module/Setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,26 @@ class Visualizer_Module_Setup extends Visualizer_Module {

const NAME = __CLASS__;

/**
* Hook that refreshes database charts.
*/
const REFRESH_DB_HOOK = 'visualizer_schedule_refresh_db';

/**
* Action Scheduler group that owns the refresh.
*/
const REFRESH_DB_GROUP = 'visualizer';

/**
* Marks the refresh trigger as checked recently.
*/
const REFRESH_DB_CHECK_TRANSIENT = 'visualizer-refresh-db-checked';

/**
* Seconds a check stays valid; matches Action Scheduler's timeout for a killed run.
*/
const REFRESH_DB_CHECK_WINDOW = 300;

/**
* Constructor.
*
Expand All @@ -44,8 +64,9 @@ public function __construct( Visualizer_Plugin $plugin ) {

register_activation_hook( VISUALIZER_BASEFILE, array( $this, 'activate' ) );
register_deactivation_hook( VISUALIZER_BASEFILE, array( $this, 'deactivate' ) );
$this->_addAction( 'visualizer_schedule_refresh_db', 'refreshDbChart' );
$this->_addAction( self::REFRESH_DB_HOOK, 'refreshDbChart' );
$this->_addAction( 'init', 'maybe_reschedule_refresh_db' );
$this->_addAction( 'action_scheduler_ensure_recurring_actions', 'ensure_refresh_db_action' );
$this->_addFilter( 'visualizer_schedule_refresh_chart', 'refresh_db_for_chart', 10, 3 );

$this->_addAction( 'admin_init', 'adminInit' );
Expand Down Expand Up @@ -485,11 +506,24 @@ public function custom_cron_schedules( $schedules ) {
* Schedule the recurring DB refresh action.
*/
private function schedule_refresh_db_action(): void {
$hook = 'visualizer_schedule_refresh_db';
$group = 'visualizer';
$hook = self::REFRESH_DB_HOOK;
$group = self::REFRESH_DB_GROUP;
$schedules = wp_get_schedules();
$interval_key = apply_filters( 'visualizer_chart_schedule_interval', 'visualizer_ten_minutes' );
$interval = $this->get_schedule_interval_seconds( $interval_key );
$timestamp = strtotime( 'midnight' ) - get_option( 'gmt_offset' ) * HOUR_IN_SECONDS;

// wp_schedule_event() refuses an unregistered schedule.
if ( ! isset( $schedules[ $interval_key ]['interval'] ) ) {
$interval_key = 'visualizer_ten_minutes';
}

$interval = isset( $schedules[ $interval_key ]['interval'] ) ? (int) $schedules[ $interval_key ]['interval'] : 600;
// gmt_offset can be fractional, and WP-Cron keys its array by this value.
$timestamp = (int) ( strtotime( 'midnight' ) - get_option( 'gmt_offset' ) * HOUR_IN_SECONDS );

// West of UTC that midnight is still ahead; start from the previous one.
if ( $timestamp > time() ) {
$timestamp -= DAY_IN_SECONDS;
}
Comment thread
Alexia-Soare marked this conversation as resolved.

if (
visualizer_can_use_action_scheduler()
Expand All @@ -498,63 +532,111 @@ private function schedule_refresh_db_action(): void {
) {
$next = as_next_scheduled_action( $hook, array(), $group );
if ( false === $next ) {
as_schedule_recurring_action( $timestamp, $interval, $hook, array(), $group );
// Unique: a concurrent request can arrive while nothing is pending.
as_schedule_recurring_action( $timestamp, $interval, $hook, array(), $group, true );

// Returns 0 on failure, so ask the store.
$next = as_next_scheduled_action( $hook, array(), $group );
}

// Drop the WP-Cron fallback only once the action exists.
if ( false !== $next ) {
wp_clear_scheduled_hook( $hook );
return;
}
wp_clear_scheduled_hook( $hook );
}

// Re-arming a live event would pin it to a past timestamp and keep it due.
$event = wp_get_scheduled_event( $hook );
if ( $event && $event->schedule === $interval_key ) {
return;
}

wp_clear_scheduled_hook( $hook );
wp_schedule_event( $timestamp, $interval_key, $hook );
// Schedule first so a refused replacement keeps the old event, then remove the old one
// by its timestamp: wp_clear_scheduled_hook() would take the new one too.
if ( false === wp_schedule_event( $timestamp, $interval_key, $hook ) ) {
return;
}

// A matching timestamp was already overwritten in place.
if ( $event && $event->timestamp !== $timestamp ) {
wp_unschedule_event( $event->timestamp, $hook );
}
Comment thread
Alexia-Soare marked this conversation as resolved.
}

/**
* Keep the DB refresh scheduled when Action Scheduler is not available.
*
* The migration to Action Scheduler clears the WP-Cron event, so a site that
* already migrated and then lost the library would have nothing left running
* the refresh. Re-arms WP-Cron in that case; no-op whenever the library is up.
* Check once per window, on init, that something still fires the refresh.
*/
public function maybe_reschedule_refresh_db(): void {
if ( get_transient( self::REFRESH_DB_CHECK_TRANSIENT ) ) {
return;
}

$this->ensure_refresh_db_action();

// Cache only a check that left a trigger; a failed one retries next request.
if ( $this->has_refresh_db_trigger() ) {
set_transient( self::REFRESH_DB_CHECK_TRANSIENT, 1, self::REFRESH_DB_CHECK_WINDOW );
}
}
Comment thread
Alexia-Soare marked this conversation as resolved.
Comment thread
Alexia-Soare marked this conversation as resolved.

/**
* Keep the DB refresh scheduled.
*
* A killed run never reaches schedule_next_instance(), so Action Scheduler's chain ends there.
*/
public function ensure_refresh_db_action(): void {
if ( ! $this->refresh_db_is_settled() ) {
$this->schedule_refresh_db_action();
}
}

/**
* Whether the refresh is on Action Scheduler with no WP-Cron event beside it.
*
* @return bool
*/
private function refresh_db_is_settled(): bool {
$hook = self::REFRESH_DB_HOOK;

if (
visualizer_can_use_action_scheduler()
&& function_exists( 'as_next_scheduled_action' )
&& function_exists( 'as_schedule_recurring_action' )
) {
return;
return false !== as_next_scheduled_action( $hook, array(), self::REFRESH_DB_GROUP )
&& ! wp_next_scheduled( $hook );
}

if ( wp_next_scheduled( 'visualizer_schedule_refresh_db' ) ) {
return;
return (bool) wp_next_scheduled( $hook );
}

/**
* Whether anything will fire the refresh hook again.
*
* @return bool
*/
private function has_refresh_db_trigger(): bool {
$hook = self::REFRESH_DB_HOOK;

if ( visualizer_can_use_action_scheduler() && function_exists( 'as_next_scheduled_action' ) ) {
if ( false !== as_next_scheduled_action( $hook, array(), self::REFRESH_DB_GROUP ) ) {
return true;
}
}

$this->schedule_refresh_db_action();
return (bool) wp_next_scheduled( $hook );
}

/**
* Unschedule the recurring DB refresh action.
*/
private function unschedule_refresh_db_action(): void {
$hook = 'visualizer_schedule_refresh_db';
$group = 'visualizer';
$hook = self::REFRESH_DB_HOOK;
$group = self::REFRESH_DB_GROUP;
if ( function_exists( 'as_unschedule_all_actions' ) ) {
as_unschedule_all_actions( $hook, array(), $group );
}
wp_clear_scheduled_hook( $hook );
}

/**
* Resolve a cron schedule key to seconds.
*
* @param string $interval_key Cron schedule key.
* @return int Interval in seconds.
*/
private function get_schedule_interval_seconds( $interval_key ) {
$schedules = wp_get_schedules();
if ( isset( $schedules[ $interval_key ]['interval'] ) ) {
return (int) $schedules[ $interval_key ]['interval'];
}

return 600;
}
}
42 changes: 0 additions & 42 deletions classes/Visualizer/Module/Upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ public static function upgrade() {
$upgraded = true;
}

if ( wp_next_scheduled( 'visualizer_schedule_refresh_db' ) ) {
self::migrate_action_scheduler();
$upgraded = true;
}

if ( ! $upgraded ) {
return;
}
Expand Down Expand Up @@ -79,41 +74,4 @@ private static function makeAllTableChartsTabular() {
);
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
}

/**
* Migrate recurring WP-Cron jobs to Action Scheduler.
*/
private static function migrate_action_scheduler(): void {
if ( ! function_exists( 'as_schedule_recurring_action' ) || ! function_exists( 'as_next_scheduled_action' ) ) {
return;
}

$hook = 'visualizer_schedule_refresh_db';
$group = 'visualizer';
$interval_key = apply_filters( 'visualizer_chart_schedule_interval', 'visualizer_ten_minutes' );
$interval = self::get_schedule_interval_seconds( $interval_key );
$timestamp = strtotime( 'midnight' ) - get_option( 'gmt_offset' ) * HOUR_IN_SECONDS;

$next = as_next_scheduled_action( $hook, array(), $group );
if ( false === $next ) {
as_schedule_recurring_action( $timestamp, $interval, $hook, array(), $group );
}

wp_clear_scheduled_hook( $hook );
}

/**
* Resolve a cron schedule key to seconds.
*
* @param string $interval_key Cron schedule key.
* @return int Interval in seconds.
*/
private static function get_schedule_interval_seconds( $interval_key ) {
$schedules = wp_get_schedules();
if ( isset( $schedules[ $interval_key ]['interval'] ) ) {
return (int) $schedules[ $interval_key ]['interval'];
}

return 600;
}
}
Loading
Loading