WP Cron на альтернативном

WP Cron на альтернативном

Не то, чтобы хвастаюсь, но с WordPress’ом работаю уже много лет — больше 10 точно. Не самый знаменательный показатель, но за это время что только не было — и простые сайтики, бложики, новостные порталы, магазины, социальные сети и LMS. В общем, кейсов достаточно.

Однако, при всём многообразии возможностей, различных функциях и прочих полезностях есть в нём одна вещь, которая несколько… не радует что ли? По крайней мере она оставляет желать лучшего. Это WP Cron.

Для тех, кто не знает, поясню — он работает лишь тогда, когда кто-то посещает страницы сайта. На фоне отправляется специальный неблокирующий запрос, который начинает в фоне выполнять все задачи, которые должны были быть выполнены. В принципе, такой подход имеет свои определённые плюсы, но и минусы там тоже имеются.

Во-первых, если нужна реализация каких-то задач, которые должны быть выполнены точно в определённое время — это проблема. Например, однажды делал себе бота в телеграм, который ровно в 21:00 присылал мне статистику и аналитику. А так как инстанс WP, на котором крутился этот бот, был запрятан в потаённых углах и никаких посетителей там быть не могло, то единственным вариантом по-умолчанию было либо на сервере создавать cron-задачу (которая бы его дёргала), либо самостоятельно после 21:00 открывать любую страницу сайта-сервиса, чтобы он уже запускал этот процесс. Но это уже ни в какие ворота…

По какой-то причине cron-задачу мне было делать на сервере лень, поэтому я написал свою реализацию фонового процесса. Возможно, когда-нибудь про это напишу, а сегодня о другом.

Делаю себе вечерами один проект. Всё хорошо, всё работает. Кроме Cron’а. Причем работает максимально странно — он вроде бы и выполняется (т.е. отложенная публикация работает, всё ок). А вот мои функции — нет. Я конечно же сначала подумал, что написал их неправильно, всё проверил — прямой вызов через REST отрабатывает корректно. Поставил плагин для наблюдения Cron-задачами — функции есть, время выполнения отсчитывается, даже момент выполнения отмечается — но функции не выполняются и всё. По советам самого уважаемого сайта даже альтернативный вариант использовал — всё мимо.

В какой-то момент ночи, когда уже занимался рассвет, я на всё плюнул и решил расчехлить практику из велосипедостроительного — не то, чтобы я этим гордился, но иногда она решает. В конце концов, если гора не хочет идти, то надо двигаться к ней самостоятельно.

Итого, что мы имеем:

  • нам нужно выполнять функции раз в 10 минут
  • функции не выполняются с CRON’а
  • сайт не закрытый и посещаемый (т.е. нет проблем с постоянным дёрганьем, есть проблемы с функциями 😎)

Родился следующий план — в WordPress есть замечательный механизм хуков, где мы можем зацепиться за любое состояние системы. В данном случае подходит больше всех init — система уже загружена полностью. Теперь нам нужно сделать так, чтобы эта функциональность не срабатывала каждый раз при запуске системы — раз в 10 минут пойдёт. И да, по сути получается тот же самый Cron, но только который должен работать.

UPD: не могу не согласиться с замечанием в комментариях, что вообще в целом такие запросы в идеале вешать на хук shutdown, когда весь основной процесс уже выполнился.

Начинаем с самого простого — где будем хранить временные метки. Первой на ум приходит база, но лишний запрос — это лишний запрос. Конечно, можно запихнуть её и в автозагружаемые опции, однако мой выбор — файл. Поэтому, определим место и название файла, где он будет храниться:

$dir = WP_CONTENT_DIR . '/service';$file = $dir . '/timestamp.lock';

Сначала мы проверяем файл на наличие и если его нет, то попытаемся создать:

if (!is_dir($dir)) {    if (!function_exists('wp_mkdir_p') || !wp_mkdir_p($dir)) {        return;    }}$fp = @fopen($file, 'c+');if (!$fp) {    return;}

Для работы с файлом и его содержимым мы будем использовать блокировку, чтобы не случилось одновременной работы с ним:

$fp = @fopen($file, 'c+');if (!flock($fp, LOCK_EX)) {    fclose($fp);    return;}

Затем мы читаем его содержимое и если текущее время превышает временную метку или равно ей, то мы указываем следующую метку времени и перезаписываем её в файл, а также разрешаем выполнение задач:

if ($current_ts === 0 || time() >= $current_ts) {    fwrite($fp, (string)$next_ts);    $should_run = true;}

Затем мы закрываем обработку файла и снимаем блокировку, после чего запускаем отдельную функцию, внутри которой уже можно вызвать что угодно:

if ($should_run) {    if (function_exists('runBackground')) {        runBackground();    }}

Да, конечно можно логику описать и в пределах if ($should_run), но я привык разделять функции по блокам и вот такую логику обычно описываю в сервисных разделах, а функции типа runBackground выносить поближе, где её при случае можно быстро найти и отредактировать.

Дополнительно, я ещё создал простенькую функцию для вывода даты и времени следующего вызова, чтобы это можно было, например, в админке вывести:

function get_update_time_message(string $prefixText = 'Данные будут обновлены'): string{    $file_path = WP_CONTENT_DIR . '/service/timestamp.lock';    if (file_exists($file_path)) {        $timestamp = file_get_contents($file_path);        if (is_numeric($timestamp)) {            return sprintf(                '%s %s',                $prefixText,                date('H:i:s d.m.Y', intval($timestamp))            );        }    }    return '';}

В результате у меня получился вот такой файл.

В итоге получился такой альтернативный Cron — без излишних амбиций, но честно выполняет свою работу. Может, он и не претендует на звание эталона фоновых задач, зато работает стабильно и не требует от тебя бубнов и танцев вокруг настроек сервера.

А если однажды решит отдохнуть и перестанет запускать функции — ну что ж, будет повод написать новую статью. С драмой, расследованием, парой ночей без сна и, конечно же, хэппи-эндом. Потому что иначе и неинтересно.