Кроссплатформенная блокировка файлов в PHP

Как её лучше всего реализовать и насколько надёжно использовать в этих целях flock()?

Настройки отображения темы Показывать по сообщений с сортировкой .
Выводить , отправленные .
Одна страница
Распечатать
10geek
Единомышленник
Всего сообщений: 238
Зарегистрирован: 29 июн 2018, 09:36
Рейтинг пользователя: 11

0
. Редактировалось 6 раз, последний — #1
Занялся доработкой своего микрофреймворка на PHP и столкнулся со следующей задачей. Во фреймворке предусмотрен встроенный планировщик задач, который не должен допускать одновременного выполнения одной и той же задачи в нескольких параллельных потоках. Такое может произойти, если во время выполнения задачи произойдёт второй запрос к серверу (или запуск параллельного процесса через cron). Насколько надёжно использовать в этих целях flock(), насколько хорошо она поддерживается в разных ОС? Если есть более предпочтительный вариант блокировок, то какой?

Из особенностей использования flock() под Windows известно, что на Windows нельзя писать в тот же файл, на котором стоит блокировка. Если подводные камни на этом заканчиваются, то flock() для данной задачи вполне подходит. Но больше всего настораживает предупреждение из официальной документации:
Внимание
В некоторых операционных системах flock() реализован на уровне процессов. При использовании многопоточных серверных API, таких как ISAPI, вы не можете полагаться на flock() для защиты ваших файлов от других PHP-скриптов, которые работают в параллельном потоке на том же сервере!

Если верить написанному, то получается, что flock() под Windows (если она подразумевается под «некоторыми операционными системами») будет корректно работать, например, с PHP-FPM, но не сработает в случае, если используется Apache с модулем PHP и в одном процессе Apache будет работать два потока, получающих блокировку на один и тот же файл.

В дополнение к этому стоит упомянуть нестандартный способ реализации блокировок, который можно использовать в качестве резервного. Но стоит ли его использовать — это большой вопрос. Дело в том, что создание каталога является атомарной операцией, а значит этим можно воспользоваться и вместо установки блокировки на файл создавать каталог через mkdir() и проверять возвращаемое ей значение, а в качестве снятия блокировки удалять каталог. Плюс способа в том, что он абсолютно кроссплатформенный и гарантированно будет работать на любых ФС. Ложка дёгтя тут заключается в том, что если во время выполнения задачи планировщика скрипт завершится с ошибкой, то каталог не удалится и следующего запуска задачи не произойдёт до тех пор, пока каталог не будет удалён вручную. Можно пойти дальше: если каталог уже существует, то проверять, сколько прошло времени с момента его создания и удалять его, если это время превышает минимальный интервал между запусками задачи (задаётся в конф. файле фреймворка). Но в таком случае если по какой-то причине задача будет выполняться дольше времени, указанного в конф. файле, то следующий запущенный процесс удалит каталог и запустит второй экземпляр задачи параллельно. Вывод становится очевиден: mkdir() не может полностью заменить flock() и его использование в любом случае чревато либо race condition'ами, либо риском в один прекрасный день обнаружить, что задача планировщика уже несколько месяцев не запускалась.

4X_Pro
Создатель сайта
Всего сообщений: 2533
Зарегистрирован: 9 дек 2015, 19:20
Рейтинг пользователя: 1264

0
. Редактировалось 5 раз, последний — #2
В своё время сталкивался с тем, что под Windows flock не работал, поэтому стараюсь его не использовать. В IntB 3 реализовал то, о чём ты говоришь через базу данных: таблица заданий блокируется через LOCK TABLE, из неё выбирается нужная задача, ставится пометка, что она уже выполняется (в случае с IntB — меняется время next_run на будущее для многоразовых задач), потом таблица разблокируется.
По поводу того, что ты предлагаешь — без базы примерно так и реализуют, только вместо каталога используют пустой вспомогательный файл (обычно с расширением lock), так как создание файла — быстрее, чем каталога, но тоже атомарная операция. Точнее, наверное, потребуется два файла: один — на весь список заданий, второй — на каждое конкретное. Причём в lock-файл со списком заданий нужно писать pid заблокировавшего его процесса, чтобы остальные могли проверить, жив ли этот процесс (аналогично pid-файлам в Linux). А чтобы сделать запись pid атомарной, нужно сначала писать в отдельный файл, а потом его переименовывать. Но всё равно это только снижает вероятность гонок, но не устраняет их полностью для случая, когда происходит захват lock-файла от убитого процесса.
Но самое надёжное — вешать на обычный системный cron скрипт, который будет запускаться каждую минуту и следить, есть ли синхронные и асинхронные задачи твоего приложения, которые нужно выполнить. Единственное что, нужно следить за временем после выполнения каждой из задач, и если прошло 59 секунд, завершаться, даже если задачи ещё остаются. Недостаток только один: не будет работать на хостингах, где не дают доступа к cron (хотя сейчас среди платных таких уже и не осталось, наверное). В частности, так сделано в InstantCMS, Oxwall, Friendica и множестве других скриптов.

Ребята, давайте жить спокойно!

10geek
Единомышленник
Всего сообщений: 238
Зарегистрирован: 29 июн 2018, 09:36
Рейтинг пользователя: 11

0
#3
4X_Pro написал(а):
В IntB 3 реализовал это через базу данных

Через базу данных — однозначно самый переносимый вариант. Но мне нужно именно без базы данных.
4X_Pro написал(а):
только вместо каталога используют пустой вспомогательный файл (обычно с расширением lock), так как создание файла — быстрее, чем каталога

Вот так делать нельзя ни в коем случае, это не исключает race conditions, только уже потому, что проверка наличия файла и его создание — это две разные операции, между которыми существует временной промежуток. Отсюда возможна ситуация, когда два параллельных потока проверяют наличие файла блокировки и, не найдя его, создают его (оба одновременно) и начинают оба выполнять задачу. Весь смысл способа с каталогом в том, что создавая его, ты тут же и проверяешь его наличие по возвращаемому mkdir() значению — это одна атомарная операция, и это исключает race condition.
4X_Pro написал(а):
Точнее, наверное, потребуется два файла: один — на весь список заданий, второй — на каждое конкретное.

Нет, второй не понадобится. В нём была бы необходимость, если бы нужно было запускать задачи параллельно.
4X_Pro написал(а):
Причём в lock-файл со списком заданий нужно писать pid заблокировавшего его процесса, чтобы остальные могли проверить, жив ли этот процесс

Та же проблема: операций две — чтение pid из файла и проверка существования процесса, а значит между этими операциями может что-нибудь произойти… Параллельные алгоритмы — это всегда «минное поле».
4X_Pro написал(а):
Но самое надёжное — вешать на обычный системный cron скрипт

Да, у меня так и сделано. Но на всякий случай нужно иметь резервный вариант без cron'а. В некоторых случаях он удобнее, а иногда и единственный возможный, но это всё же редкость.
4X_Pro написал(а):
если прошло 59 секунд, завершаться, даже если задачи ещё остаются

Была идея получать значение max_execution_time и, если каталог уже существует и старше max_execution_time, то удалять его. Но в документации нас предупреждают:
Функция set_time_limit() и директива max_execution_time влияют на время выполнения только самого скрипта. Время, затраченное на различные действия вне скрипта, такие как системные вызовы функции system(), потоковые операции, запросы к базам данных и т.п. не включаются в расчёт времени выполнения скрипта. Это не относится к системам Windows, где рассчитывается абсолютное время выполнения.

Так что способ с каталогом, видимо, отбраковывается. Самое лучшее — это блокировки на уровне ФС. А нет ли в PHP поддержки какого-нибудь ещё механизма блокировок, помимо flock()?

4X_Pro
Создатель сайта
Всего сообщений: 2533
Зарегистрирован: 9 дек 2015, 19:20
Рейтинг пользователя: 1264

1
. Редактировалось 2 раза, последний — #4
10geek написал(а):
Весь смысл способа с каталогом в том, что создавая его, ты тут же и проверяешь его наличие по возвращаемому mkdir() значению — это одна атомарная операция, и это исключает race condition.

Вообще, в php есть для fopen опция 'x', которая судя по документации, сводится к выполнению системного вызова open с флагами O_EXCL|O_CREAT. И тогда это тоже получается атомарная операция, которая возвращает либо ошибку, если файл уже есть, либо создаёт его и возвращает resource-дескриптор.
Кстати, если PHP запускается через командную строку (или тот же системный cron), то max_execution_time не учитывается.

Ребята, давайте жить спокойно!

10geek
Единомышленник
Всего сообщений: 238
Зарегистрирован: 29 июн 2018, 09:36
Рейтинг пользователя: 11

0
#5
4X_Pro написал(а):
Вообще, в php есть для fopen опция 'x', которая судя по документации, сводится к выполнению системного вызова open с флагами O_EXCL|O_CREAT. И тогда это тоже получается атомарная операция, которая возвращает либо ошибку, если файл уже есть, либо создаёт его и возвращает resource-дескриптор.

Насчёт O_EXCL — спасибо, буду знать.
4X_Pro написал(а):
Кстати, если PHP запускается через командную строку (или тот же системный cron), то max_execution_time не учитывается.

Даже если он переопределён через set_time_limit()?

4X_Pro
Создатель сайта
Всего сообщений: 2533
Зарегистрирован: 9 дек 2015, 19:20
Рейтинг пользователя: 1264

0
#6
Кстати, а вариант использовать SQLite для этих целей ты не рассматривал?

Ребята, давайте жить спокойно!

10geek
Единомышленник
Всего сообщений: 238
Зарегистрирован: 29 июн 2018, 09:36
Рейтинг пользователя: 11

0
. Редактировалось 1 раз, последний — #7
4X_Pro написал(а):
Кстати, а вариант использовать SQLite для этих целей ты не рассматривал?

Мысль такая приходила. И да, это было бы кроссплатформенно. Но это оправданно, на мой взгляд, только если в проекте и так используется SQLite. Не хотелось бы делать зависимость от SQLite ради одного только планировщика.

Одна страница
Распечатать

У вас нет прав для отправки сообщений в эту тему.

Задать вопрос

Здесь можно задать мне вопрос или спросить совета по любой теме, затронутой в блогах или на форуме. После того, как я отвечу, вопрос и ответ появятся в соответствующем разделе. Но не забываем, что я — сторонник slow life, поэтому каких-либо сроков ответов не обещаю. Самые интересные вопросы станут основой для новых тем на форуме или записей в блоге.
Сразу предупреждаю: глупости, провокации, троллинг и тому подобное летит прямо в /dev/null.