Функция wp_schedule_single_event возвращает true или false, и почти никто это значение не проверяет. А у неё есть правило, которое делает её ненадёжной для «разовой работы через N минут»: если такое же событие уже стоит в пределах 10 минут, второе молча не ставится.
Это тридцать шестая часть цикла «Кэш страниц без MISS».
Как это выстрелило
После выката нужно было вынести старые ключи кэша фрагментов из Redis (подробно — во втором цикле). Чистку повесили разовым событием через 180 секунд после выката. Она не выполнилась.
Итог: 32 981 мёртвый ключ и 200 МБ памяти вместо 25 провисели 20 минут — пока кто-то не заметил.
Причина: такое же событие уже стояло в пределах десяти минут (от предыдущей попытки), и WordPress счёл новое дублем.
Приём: вешать на уже идущий воркер
Любую важную разовую работу надёжнее делать не отдельным событием, а проверкой внутри периодического воркера, который и так работает. И сравнивать не время, а версию кода:
// внутри воркера, который работает каждые 5 минут
function cache_gc_watchdog(): void {
$code = cache_code_version(); // версия текущей сборки
if (get_option('cache_gc_done_for') === $code) {
return; // для этой версии уже чистили
}
$removed = cache_delete_dead_generations($code);
update_option('cache_gc_done_for', $code, false);
error_log("cache gc: removed {$removed} keys");
}
Сработало в первую же пятиминутку: 4568 ключей за 0,03 с.
Ещё одна граница: 60 секунд
Константа WP_CRON_LOCK_TIMEOUT по умолчанию — 60 секунд. Если прогон дольше, следующий запуск крона может решить, что предыдущий умер, и пойти параллельно. Длинным прогонам нужен свой замок (тридцать пятая часть), а не надежда на встроенный.
Проверяйте возвращаемое значение wp_schedule_single_event хотя бы в журнале. false без ошибки — ровно этот случай.
Отложенные задачи на сайте иногда не выполняются?
Пришлите адрес — проверю, не съедает ли их правило десяти минут.
// загружаю комментарии…