Контроль выполнения PHP кода в реальном времени

Раздел: Программирование -> Диагностика

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

Способы диагностики выполнения скрипта

Как убедиться, что скрипт уже запущен, используя файловую блокировку?

Цель:

исключить повторный запуск одного и того же скрипта или координировать параллельные процессы.
$lockFile = '/tmp/my_script.lock';
$fp = fopen($lockFile, 'c');
if (flock($fp, LOCK_EX | LOCK_NB)) {
    // блокировка захвачена – скрипт не выполняется
    echo "Скрипт запущен впервые\n";
    // выполнение основной работы
    sleep(30);
    flock($fp, LOCK_UN);
    fclose($fp);
    @unlink($lockFile);
} else {
    // блокировка не получена – другой экземпляр уже работает
    exit("Скрипт уже выполняется\n");
}

Php скрипт выполняется (статус выполнения php скрипта)

Пояснение: функция flock() устанавливает монопольную блокировку на открытый файл. Флаг LOCK_NB делает попытку неблокирующей: если блокировка уже удерживается другим процессом, вызов возвращает false. После завершения работы блокировка снимается, файл блокировки удаляется. Метод прост и эффективен, не требует внешних служб.

Типичные ошибки: скрипт может аварийно завершиться, не сняв блокировку. Для автоматического освобождения стоит использовать конструкцию try/finally или регистрировать обработчик сигналов. Также при монтировании файловой системы на NFS блокировки могут работать нестабильно.

Случаи применения:

ограничение одновременного выполнения cron задач, защита от дублирования обработчиков вебхуков.

Как определить статус выполнения через запись в базу данных?

Цель:

хранить информацию о запуске в централизованном хранилище для мониторинга нескольких серверов.
$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$scriptId = 'my_long_script';
$startTime = time();

// Попытка вставить запись с уникальным идентификатором
$stmt = $db->prepare('INSERT INTO process_status (script_id, start_time) VALUES (?, ?)');
if ($stmt->execute([$scriptId, $startTime])) {
    // запись создана – скрипт не выполняется
    // выполнение работы
    // после завершения удалить запись
    $db->exec('DELETE FROM process_status WHERE script_id="' . $scriptId . '"');
} else {
    // запись уже существует – скрипт выполняется
    exit('Скрипт уже запущен');
}

Php error reporting e all (настройка отображения всех ошибок php (e_all))

Пояснение: предполагается, что колонка script_id имеет уникальный индекс. Первый INSERT выполнится успешно, последующие вызовы получат ошибку дубликата. Это надёжный способ для распределённых систем, но требует наличия БД.

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

Случаи применения:

проекты с централизованной БД, где несколько серверов могут запускать один скрипт.

Как использовать APCu кеш для отметки выполнения скрипта?

Цель:

лёгкий, бездисковый вариант для одного сервера.
if (!apcu_add('script_running', 1, 3600)) {
    // ключ уже существует – другой процесс выполняется
    exit('Скрипт уже работает');
}
// выполнение работы
register_shutdown_function(function() {
    apcu_delete('script_running');
});

Info php start (информация о php)

Пояснение: apcu_add() сохраняет значение только в том случае, если ключ отсутствует. При повторном вызове возвращает false. TTL (3600 секунд) обеспечивает автоматическое удаление при сбое. Функция shutdown гарантирует удаление ключа при нормальном завершении.

Важно: APCu может быть недоступен, если модуль не установлен. В случае фатальной ошибки shutdown может не сработать, поэтому TTL служит страховкой. Ключ виден только в рамках одного сервера.

Случаи применения:

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

Можно ли проверить выполнение скрипта через команду ps и её результат?

Цель:

внешняя проверка без изменения кода самого скрипта.
$output = [];
exec('ps aux | grep my_script.php | grep -v grep', $output, $exitCode);
if (count($output) > 0) {
    echo 'Скрипт выполняется';
} else {
    echo 'Скрипт не выполняется';
}

Пояснение: утилита ps возвращает список процессов. Фильтр grep my_script.php отбирает строки, содержащие имя файла. Исключение grep -v grep убирает саму команду grep из вывода. Количество строк больше нуля означает, что процесс существует.

Ошибки: на разных ОС синтаксис ps различается. Результат может включать несколько процессов, если запущено несколько экземпляров. Для точной идентификации нужно сопоставлять PID. Также команда может быть отключена в целях безопасности.

Случаи применения:

мониторинг извне, когда нельзя модифицировать исходный скрипт, или как вспомогательный способ отладки.

Дополнительные расширенные примеры

Пример 1. Обработка сигналов для корректного снятия блокировки

При использовании файловой блокировки важно освобождать ресурс даже при получении сигнала завершения. В PHP можно задать обработчики для SIGTERM и SIGINT.

Пример
$lockFile = '/tmp/my_daemon.lock';
$fp = fopen($lockFile, 'c');
if (!flock($fp, LOCK_EX | LOCK_NB)) {
    exit("Процесс уже запущен\n");
}

declare(ticks = 1);
pcntl_signal(SIGTERM, function() use ($fp, $lockFile) {
    flock($fp, LOCK_UN);
    fclose($fp);
    @unlink($lockFile);
    exit("Принудительное завершение\n");
});
pcntl_signal(SIGINT, function() use ($fp, $lockFile) {
    flock($fp, LOCK_UN);
    fclose($fp);
    @unlink($lockFile);
    exit("Завершение по Ctrl+C\n");
});

echo "Работа запущена, PID=" . getmypid() . "\n";
sleep(300); // имитация долгой работы

flock($fp, LOCK_UN);
fclose($fp);
@unlink($lockFile);
echo "Работа завершена\n";

Результат выполнения (при преждевременном завершении сигналом SIGTERM):

Работа запущена, PID=12345
Принудительное завершение

При этом файл .lock удаляется. Без обработчиков скрипт оставит файл, и следующий запуск будет невозможен.

Пример 2. Использование shared memory (shmop) для уведомления о статусе

Для межпроцессного взаимодействия без файлов можно использовать разделяемую память. В PHP функции shmop позволяют записывать и читать данные, которые видны всем процессам.

Пример
$shmKey = ftok(__FILE__, 't');
$shmId = shmop_open($shmKey, 'c', 0644, 1);
if (!$shmId) {
    exit("Не удалось создать сегмент памяти\n");
}

// Пытаемся установить значение 1 (занято) с помощью атомарной операции
$current = shmop_read($shmId, 0, 1);
if ($current === "1") {
    shmop_close($shmId);
    exit("Скрипт уже выполняется\n");
} else {
    shmop_write($shmId, "1", 0);
    shmop_close($shmId);
}

register_shutdown_function(function() use ($shmKey) {
    $shmId = shmop_open($shmKey, 'w', 0, 0);
    if ($shmId) {
        shmop_write($shmId, "0", 0);
        shmop_close($shmId);
    }
});

echo "Запуск процесса\n";
sleep(20); // работа

echo "Завершение\n";
// очистка уже выполнена в shutdown

Результат при повторном запуске:

Скрипт уже выполняется

Метод быстр и не создаёт файлов, но требует конфигурации shmop в PHP.

Пример 3. Запуск дочернего скрипта через proc_open и мониторинг его состояния

Иногда нужно запустить PHP скрипт как подпроцесс и отслеживать его жизнедеятельность. Функция proc_open возвращает ресурс, по которому можно проверять статус.

Пример
$descriptorspec = [
    0 => ['pipe', 'r'],
    1 => ['pipe', 'w'],
    2 => ['pipe', 'w'],
];

$process = proc_open('php child_worker.php', $descriptorspec, $pipes);

if (is_resource($process)) {
    fclose($pipes[0]);
    $status = proc_get_status($process);
    echo "PID: " . $status['running'] ? $status['pid'] : 'unknown' . "\n";

    // Имитация ожидания
    sleep(2);

    $status = proc_get_status($process);
    if ($status['running']) {
        echo "Дочерний процесс ещё выполняется\n";
    } else {
        echo "Дочерний процесс завершён\n";
    }

    proc_close($process);
}

Содержимое child_worker.php:

Пример
<?php
sleep(5);
echo "Работа завершена";

Результат:

PID: 23456
Дочерний процесс ещё выполняется

Метод позволяет управлять временем жизни дочернего процесса и перехватывать его вывод.

Пример 4. Комбинированный подход с использованием MySQL и блокировкой строк

Для критических сценариев можно использовать SELECT ... FOR UPDATE в InnoDB, чтобы заблокировать строку статуса на уровне базы данных. Это гарантирует атомарность проверки и обновления.

Пример
try {
    $db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
    $db->beginTransaction();

    // Выбираем запись о статусе скрипта с блокировкой строки
    $stmt = $db->prepare('SELECT status FROM script_status WHERE script_id = ? FOR UPDATE');
    $stmt->execute(['my_script']);
    $row = $stmt->fetch();

    if ($row && $row['status'] === 'running') {
        $db->rollBack();
        exit('Скрипт уже выполняется');
    }

    // Устанавливаем статус 'running'
    $update = $db->prepare('INSERT INTO script_status (script_id, status) VALUES (?, \'running\') ON DUPLICATE KEY UPDATE status = \'running\'');
    $update->execute(['my_script']);

    $db->commit();

    // Основная работа
    echo "Начало выполнения\n";
    sleep(30);

    // После завершения сбрасываем статус
    $db->exec("UPDATE script_status SET status = 'finished' WHERE script_id = 'my_script'");

} catch (Exception $e) {
    $db->rollBack();
    exit('Ошибка: ' . $e->getMessage());
}

Результат при одновременном запуске двух экземпляров:

Cкрипт уже выполняется

Этот способ подходит для распределённых систем с поддержкой транзакций.

Статус выполнения PHP скрипта - comments

En
Php скрипт выполняется (php)