Migrate all of Woo cron jobs to use action scheduler (#59325)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Seghir Nadir 2025-07-17 15:29:23 +02:00 committed by GitHub
parent bf1915fcf2
commit 1e883791e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 137 additions and 66 deletions

View file

@ -0,0 +1,4 @@
Significance: minor
Type: update
Migrate all cron jobs (session clean up, unpaid order clean up, sale schedules, log clean up...) to use action scheduler instead.

View file

@ -561,7 +561,7 @@ class WC_Install {
self::create_roles();
self::setup_environment();
self::create_terms();
self::create_cron_jobs();
self::clear_cron_jobs();
self::delete_obsolete_notes();
self::create_files();
self::maybe_create_pages();
@ -870,20 +870,20 @@ class WC_Install {
*/
public static function cron_schedules( $schedules ) {
$schedules['monthly'] = array(
'interval' => 2635200,
'interval' => MONTH_IN_SECONDS,
'display' => __( 'Monthly', 'woocommerce' ),
);
$schedules['fifteendays'] = array(
'interval' => 1296000,
'interval' => 15 * DAY_IN_SECONDS,
'display' => __( 'Every 15 Days', 'woocommerce' ),
);
return $schedules;
}
/**
* Create cron jobs (clear them first).
* Removes old cron jobs now that we moved to Action Scheduler.
*/
private static function create_cron_jobs() {
private static function clear_cron_jobs() {
wp_clear_scheduled_hook( 'woocommerce_scheduled_sales' );
wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );
wp_clear_scheduled_hook( 'woocommerce_cleanup_sessions' );
@ -892,44 +892,6 @@ class WC_Install {
wp_clear_scheduled_hook( 'woocommerce_geoip_updater' );
wp_clear_scheduled_hook( 'woocommerce_tracker_send_event' );
wp_clear_scheduled_hook( 'woocommerce_cleanup_rate_limits' );
$ve = get_option( 'gmt_offset' ) > 0 ? '-' : '+';
wp_schedule_event( strtotime( '00:00 tomorrow ' . $ve . absint( get_option( 'gmt_offset' ) ) . ' HOURS' ), 'daily', 'woocommerce_scheduled_sales' );
$held_duration = get_option( 'woocommerce_hold_stock_minutes', '60' );
if ( '' !== $held_duration ) {
/**
* Determines the interval at which to cancel unpaid orders in minutes.
*
* @since 5.1.0
*/
$cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) );
wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' );
}
// Delay the first run of `woocommerce_cleanup_personal_data` by 10 seconds
// so it doesn't occur in the same request. WooCommerce Admin also schedules
// a daily cron that gets lost due to a race condition. WC_Privacy's background
// processing instance updates the cron schedule from within a cron job.
wp_schedule_event( time() + 10, 'daily', 'woocommerce_cleanup_personal_data' );
wp_schedule_event( time() + ( 3 * HOUR_IN_SECONDS ), 'daily', 'woocommerce_cleanup_logs' );
wp_schedule_event( time() + ( 6 * HOUR_IN_SECONDS ), 'twicedaily', 'woocommerce_cleanup_sessions' );
wp_schedule_event( time() + MINUTE_IN_SECONDS, 'fifteendays', 'woocommerce_geoip_updater' );
/**
* How frequent to schedule the tracker send event.
*
* @since 2.3.0
*/
wp_schedule_event( time() + 10, apply_filters( 'woocommerce_tracker_event_recurrence', 'daily' ), 'woocommerce_tracker_send_event' );
wp_schedule_event( time() + ( 3 * HOUR_IN_SECONDS ), 'daily', 'woocommerce_cleanup_rate_limits' );
if ( ! wp_next_scheduled( 'wc_admin_daily' ) ) {
wp_schedule_event( time(), 'daily', 'wc_admin_daily' );
}
// Note: this is potentially redundant when the core package exists.
wp_schedule_single_event( time() + 10, 'generate_category_lookup_table' );
}
/**

View file

@ -309,6 +309,7 @@ final class WooCommerce {
add_action( 'woocommerce_updated', array( $this, 'add_woocommerce_remote_variant' ) );
add_action( 'woocommerce_newly_installed', 'wc_set_hooked_blocks_version', 10 );
add_action( 'update_option_woocommerce_allow_tracking', array( $this, 'get_tracking_history' ), 10, 2 );
add_action( 'action_scheduler_schedule_recurring_actions', array( $this, 'register_recurring_actions' ) );
add_filter( 'robots_txt', array( $this, 'robots_txt' ) );
add_filter( 'wp_plugin_dependencies_slug', array( $this, 'convert_woocommerce_slug' ) );
@ -1384,6 +1385,74 @@ final class WooCommerce {
update_option( 'woocommerce_allow_tracking_last_modified', time() );
}
/**
* Register recurring actions.
*/
public function register_recurring_actions() {
// Check if Action Scheduler is available.
if ( ! function_exists( 'as_schedule_recurring_action' ) || ! function_exists( 'as_schedule_single_action' ) ) {
return;
}
$ve = get_option( 'gmt_offset' ) > 0 ? '-' : '+';
// Schedule daily sales event at midnight tomorrow.
$scheduled_sales_time = strtotime( '00:00 tomorrow ' . $ve . absint( get_option( 'gmt_offset' ) ) . ' HOURS' );
as_schedule_recurring_action( $scheduled_sales_time, DAY_IN_SECONDS, 'woocommerce_scheduled_sales', array(), 'woocommerce', true );
$held_duration = get_option( 'woocommerce_hold_stock_minutes', '60' );
if ( '' !== $held_duration ) {
/**
* Determines the interval at which to cancel unpaid orders in minutes.
*
* @since 5.1.0
*/
$cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) );
as_schedule_single_action( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders', array(), 'woocommerce', true );
}
// Delay the first run of `woocommerce_cleanup_personal_data` by 10 seconds
// so it doesn't occur in the same request. WooCommerce Admin also schedules
// a daily cron that gets lost due to a race condition. WC_Privacy's background
// processing instance updates the cron schedule from within a cron job.
as_schedule_recurring_action( time() + 10, DAY_IN_SECONDS, 'woocommerce_cleanup_personal_data', array(), 'woocommerce', true );
// Schedule daily cleanup logs at 3 AM.
as_schedule_recurring_action( time() + ( 3 * HOUR_IN_SECONDS ), DAY_IN_SECONDS, 'woocommerce_cleanup_logs', array(), 'woocommerce', true );
// Schedule twice daily cleanup sessions at 6 AM and 6 PM.
as_schedule_recurring_action( time() + ( 6 * HOUR_IN_SECONDS ), 12 * HOUR_IN_SECONDS, 'woocommerce_cleanup_sessions', array(), 'woocommerce', true );
// Schedule geoip updater every 15 days.
as_schedule_recurring_action( time() + MINUTE_IN_SECONDS, 15 * DAY_IN_SECONDS, 'woocommerce_geoip_updater', array(), 'woocommerce', true );
/**
* How frequent to schedule the tracker send event.
*
* @since 2.3.0
*/
$tracker_recurrence = apply_filters( 'woocommerce_tracker_event_recurrence', 'daily' );
$core_internals = wp_get_schedules();
as_schedule_recurring_action( time() + 10, $core_internals[ $tracker_recurrence ]['interval'], 'woocommerce_tracker_send_event', array(), 'woocommerce', true );
// Schedule daily cleanup rate limits at 3 AM.
as_schedule_recurring_action( time() + ( 3 * HOUR_IN_SECONDS ), DAY_IN_SECONDS, 'woocommerce_cleanup_rate_limits', array(), 'woocommerce', true );
as_schedule_recurring_action( time(), DAY_IN_SECONDS, 'wc_admin_daily', array(), 'woocommerce', true );
// Note: this is potentially redundant when the core package exists.
as_schedule_single_action( time() + 10, 'generate_category_lookup_table', array(), 'woocommerce', true );
}
/**
* Initialize the customizer on the plugins_loaded action.
* If WooCommerce is network activated, wp_is_block_theme() will be called too early,

View file

@ -1225,11 +1225,28 @@ add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_num_de
function wc_format_option_hold_stock_minutes( $value, $option, $raw_value ) {
$value = ! empty( $raw_value ) ? absint( $raw_value ) : ''; // Allow > 0 or set to ''.
wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );
// Clear existing scheduled events.
if ( function_exists( 'as_unschedule_all_actions' ) ) {
as_unschedule_all_actions( 'woocommerce_cancel_unpaid_orders' );
} else {
wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );
}
if ( '' !== $value ) {
/**
* Filters the interval at which to cancel unpaid orders in minutes.
*
* @since 5.1.0
*
* @param int $cancel_unpaid_interval The interval at which to cancel unpaid orders in minutes.
*/
$cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $value ) );
wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' );
if ( function_exists( 'as_schedule_single_action' ) ) {
as_schedule_single_action( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders', array(), 'woocommerce', true );
} else {
wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' );
}
}
return $value;

View file

@ -1075,13 +1075,32 @@ add_action( 'woocommerce_trash_order', 'wc_update_coupon_usage_counts' );
* Cancel all unpaid orders after held duration to prevent stock lock for those products.
*/
function wc_cancel_unpaid_orders() {
$held_duration = get_option( 'woocommerce_hold_stock_minutes' );
$held_duration = get_option( 'woocommerce_hold_stock_minutes', '60' );
// Re-schedule the event before cancelling orders
// this way in case of a DB timeout or (plugin) crash the event is always scheduled for retry.
wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );
/**
* Filters the interval at which to cancel unpaid orders in minutes.
*
* @since 5.1.0
*
* @param int $cancel_unpaid_interval The interval at which to cancel unpaid orders in minutes.
*/
$cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) );
wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' );
// Clear existing scheduled events.
if ( function_exists( 'as_unschedule_all_actions' ) ) {
as_unschedule_all_actions( 'woocommerce_cancel_unpaid_orders' );
} else {
wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );
}
// Schedule the next event using Action Scheduler if available, otherwise fall back to WordPress cron.
if ( function_exists( 'as_schedule_single_action' ) ) {
as_schedule_single_action( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders', array(), 'woocommerce', true );
} else {
wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' );
}
if ( $held_duration < 1 || 'yes' !== get_option( 'woocommerce_manage_stock' ) ) {
return;

View file

@ -51,9 +51,6 @@ class OrderCountCacheService {
add_action( 'woocommerce_before_delete_order', array( $this, 'update_on_order_deleted' ), 10, 2 );
add_action( self::BACKGROUND_EVENT_HOOK, array( $this, 'refresh_cache' ) );
add_action( 'action_scheduler_ensure_recurring_actions', array( $this, 'schedule_background_actions' ) );
// This is a temporary fix to ensure the background actions are scheduled.
// @todo: Remove this once the Action Scheduler package is updated to >= 3.9.3.
add_action( 'admin_init', array( $this, 'schedule_background_actions' ) );
if ( defined( 'WC_PLUGIN_BASENAME' ) ) {
add_action( 'deactivate_' . WC_PLUGIN_BASENAME, array( $this, 'unschedule_background_actions' ) );

View file

@ -152,7 +152,7 @@ class WC_Admin_Tests_Reports_Regenerate_Batching extends WC_REST_Unit_Test_Case
)
);
// Verify that a second follow up action was queued.
WC_Helper_Queue::run_all_pending();
WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
$this->assertCount(
2,
OrdersScheduler::queue()->search(
@ -175,7 +175,7 @@ class WC_Admin_Tests_Reports_Regenerate_Batching extends WC_REST_Unit_Test_Case
)
);
// Verify that no follow up action was queued.
WC_Helper_Queue::run_all_pending();
WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
$this->assertCount(
0,
OrdersScheduler::queue()->search(

View file

@ -81,18 +81,6 @@ class WC_Admin_Tests_Install extends WP_UnitTestCase {
}
}
/**
* By the time we hit this test method, we should have the following cron jobs.
* - wc_admin_daily
* - generate_category_lookup_table
*
* @return void
*/
public function test_cron_job_creation() {
$this->assertNotFalse( wp_next_scheduled( 'wc_admin_daily' ) );
$this->assertNotFalse( wp_next_scheduled( 'generate_category_lookup_table' ) );
}
/**
* Data provider that returns DB Update version string and # of expected pending jobs.
*
@ -234,5 +222,4 @@ class WC_Admin_Tests_Install extends WP_UnitTestCase {
$this->assertNotFalse( get_option( $new_option ), $new_option );
}
}
}

View file

@ -14,6 +14,7 @@ global $wpdb, $wp_version, $wc_uninstalling_plugin;
$wc_uninstalling_plugin = true;
// Clear WordPress cron events.
wp_clear_scheduled_hook( 'woocommerce_scheduled_sales' );
wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );
wp_clear_scheduled_hook( 'woocommerce_cleanup_sessions' );
@ -26,6 +27,21 @@ wp_clear_scheduled_hook( 'wc_admin_daily' );
wp_clear_scheduled_hook( 'generate_category_lookup_table' );
wp_clear_scheduled_hook( 'wc_admin_unsnooze_admin_notes' );
// Clear Action Scheduler events.
if ( function_exists( 'as_unschedule_all_actions' ) ) {
as_unschedule_all_actions( 'woocommerce_scheduled_sales' );
as_unschedule_all_actions( 'woocommerce_cancel_unpaid_orders' );
as_unschedule_all_actions( 'woocommerce_cleanup_sessions' );
as_unschedule_all_actions( 'woocommerce_cleanup_personal_data' );
as_unschedule_all_actions( 'woocommerce_cleanup_logs' );
as_unschedule_all_actions( 'woocommerce_geoip_updater' );
as_unschedule_all_actions( 'woocommerce_tracker_send_event' );
as_unschedule_all_actions( 'woocommerce_cleanup_rate_limits' );
as_unschedule_all_actions( 'wc_admin_daily' );
as_unschedule_all_actions( 'generate_category_lookup_table' );
as_unschedule_all_actions( 'wc_admin_unsnooze_admin_notes' );
}
/*
* Only remove ALL product and page data if WC_REMOVE_ALL_DATA constant is set to true in user's
* wp-config.php. This is to prevent data loss when deleting the plugin from the backend