fix(cron): self-heal scheduled events on every boot + on plugin upgrade
Pilot on washanyanya.ru lost the cf7stg_dispatch cron event somewhere
between 21.04 and 01.05 — wp_next_scheduled('cf7stg_dispatch') returned
NULL, queue had 6 pending rows, sent-24h=0, failed=0, no error trail.
Settings/token were intact. The most likely cause is the PUC self-update
to 1.2.0: WordPress plugin upgrades replace files but do NOT call
register_activation_hook, so Activator::activate() never re-ran and any
cron event Activator::deactivate() had cleared (or that was lost for any
other reason) stayed missing.
Two-part fix:
1. Activator::ensure_scheduled() — extracted from the old private
schedule_events(), now public and idempotent. wp_next_scheduled()
short-circuits when events are already queued, so it's safe to call
on every request. Plugin::boot() invokes it on plugins_loaded — every
admin or front-end hit auto-heals lost cron events.
2. Activator::on_upgrade_complete() bound to upgrader_process_complete.
When WordPress finishes upgrading THIS plugin (matched via
plugin_basename + $hook_extra['plugins']), we re-run install_table()
(idempotent dbDelta), seed_silent_drop_options() (uses add_option,
preserves user values), and ensure_scheduled(). guard_requirements
is intentionally skipped — wp_die mid-upgrade would leave admin in
a broken state, and the host obviously meets requirements since the
old version is currently running.
Tests untouched (this is integration code wrapping WP-only APIs); full
suite still 126/126 green.
Changelog entry for 1.2.1 added to readme.txt.
Stable tag and CF7STG_VERSION will be bumped by release.sh.
Refs: cp-1lg
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ final class Activator
|
||||
self::guard_requirements();
|
||||
self::install_table();
|
||||
self::seed_silent_drop_options();
|
||||
self::schedule_events();
|
||||
self::ensure_scheduled();
|
||||
}
|
||||
|
||||
public static function deactivate(): void
|
||||
@@ -22,6 +22,63 @@ final class Activator
|
||||
wp_clear_scheduled_hook('cf7stg_cleanup');
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-heal cron events. Idempotent — safe to call from Plugin::boot()
|
||||
* on every request. If wp_next_scheduled() shows the hook isn't queued,
|
||||
* re-schedule it. This recovers from any cause of cron loss (manual
|
||||
* wp_clear_scheduled_hook, plugin upgrade that bypassed activation,
|
||||
* truncated wp_options.cron value) without requiring deactivate/activate
|
||||
* by the user.
|
||||
*
|
||||
* The cron_schedules filter must be in place inside this same request so
|
||||
* 'cf7stg_every_minute' is a known recurrence at wp_schedule_event time.
|
||||
* Dispatcher::register_hooks() also registers it on plugins_loaded, but
|
||||
* we add it here too in case ensure_scheduled() runs from a context where
|
||||
* Dispatcher's registration hasn't fired yet (e.g. during an upgrade hook).
|
||||
*/
|
||||
public static function ensure_scheduled(): void
|
||||
{
|
||||
add_filter('cron_schedules', ['\\Cf7stg\\Dispatcher', 'register_schedule']);
|
||||
|
||||
if (!wp_next_scheduled('cf7stg_dispatch')) {
|
||||
wp_schedule_event(time() + 60, 'cf7stg_every_minute', 'cf7stg_dispatch');
|
||||
}
|
||||
if (!wp_next_scheduled('cf7stg_cleanup')) {
|
||||
wp_schedule_event(time() + 3600, 'daily', 'cf7stg_cleanup');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* upgrader_process_complete callback. WordPress fires this after a
|
||||
* plugin / theme / core update completes. Plugin updates do NOT call
|
||||
* register_activation_hook, so any cron events the user's old install
|
||||
* relied on must be re-installed explicitly here. We also re-run the
|
||||
* table installer (idempotent dbDelta) and re-seed silent-drop options
|
||||
* (uses add_option, so user values are preserved).
|
||||
*
|
||||
* @param mixed $upgrader WP_Upgrader instance (unused).
|
||||
* @param array $hook_extra ['action'=>'update','type'=>'plugin','plugins'=>[...]]
|
||||
*/
|
||||
public static function on_upgrade_complete($upgrader, $hook_extra): void
|
||||
{
|
||||
if (!is_array($hook_extra)) return;
|
||||
if (($hook_extra['action'] ?? '') !== 'update') return;
|
||||
if (($hook_extra['type'] ?? '') !== 'plugin') return;
|
||||
|
||||
$plugins = (array)($hook_extra['plugins'] ?? []);
|
||||
$self = plugin_basename(CF7STG_PLUGIN_FILE);
|
||||
if (!in_array($self, $plugins, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip guard_requirements() — already running on a live install whose
|
||||
// PHP/WP versions are clearly compatible. wp_die here would interrupt
|
||||
// the upgrader and leave admin in a broken state.
|
||||
self::install_table();
|
||||
self::seed_silent_drop_options();
|
||||
self::ensure_scheduled();
|
||||
}
|
||||
|
||||
private static function guard_requirements(): void
|
||||
{
|
||||
if (version_compare(PHP_VERSION, '7.4', '<')) {
|
||||
@@ -86,20 +143,4 @@ final class Activator
|
||||
]);
|
||||
}
|
||||
|
||||
private static function schedule_events(): void
|
||||
{
|
||||
// Register the custom 60-second schedule here — Dispatcher::register_hooks()
|
||||
// runs on plugins_loaded, which is AFTER activation. Without this filter
|
||||
// being in place during activate(), wp_schedule_event with an unknown
|
||||
// recurrence ('cf7stg_every_minute') silently fails and the event never
|
||||
// fires.
|
||||
add_filter('cron_schedules', ['\\Cf7stg\\Dispatcher', 'register_schedule']);
|
||||
|
||||
if (!wp_next_scheduled('cf7stg_dispatch')) {
|
||||
wp_schedule_event(time() + 60, 'cf7stg_every_minute', 'cf7stg_dispatch');
|
||||
}
|
||||
if (!wp_next_scheduled('cf7stg_cleanup')) {
|
||||
wp_schedule_event(time() + 3600, 'daily', 'cf7stg_cleanup');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user