PHP - Предотвращение конфликтов в Cron - Безопасная блокировка файлов?

Я пытаюсь найти безопасный способ предотвратить конфликт заданий cron (т.е. предотвратить его запуск, если другой экземпляр уже запущен).

Некоторые варианты, которые я нашел, рекомендуют использовать блокировка файла.

Это действительно безопасный вариант? Что произойдет, если, например, скрипт умрет? Замок останется?

Есть ли другие способы сделать это?


person Ben    schedule 25.03.2011    source источник
comment
Если вы откроете файл для записи, разве он уже не привязан к одному процессу?   -  person GWW    schedule 25.03.2011
comment
@zerkms: Думаю, мне действительно нужно это пересмотреть, спасибо.   -  person GWW    schedule 25.03.2011
comment
нет, это решение не подходит. Это влияет на бесконечную блокировку, если процесс умер и состояние гонки. Лучшим решением было бы использовать flock   -  person zerkms    schedule 25.03.2011
comment
если сценарий умирает, блокировка, полученная flock, будет снята.   -  person zerkms    schedule 25.03.2011


Ответы (3)


Этот образец был взят на http://php.net/flock и немного изменен, и это <сильный > правильный способ делать то, что вы хотите:

$fp = fopen("/path/to/lock/file", "w+");
if (flock($fp, LOCK_EX | LOCK_NB)) { // do an exclusive lock
  // do the work
  flock($fp, LOCK_UN); // release the lock
} else {
  echo "Couldn't get the lock!";
}
fclose($fp);

Не используйте такие места, как /tmp или /var/tmp, так как они могут быть очищены вашей системой в любое время, что приведет к нарушению вашей блокировки в соответствии с документами:

Программы не должны предполагать, что какие-либо файлы или каталоги в / tmp сохраняются между вызовами программы.

https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch03s18.html https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch05s15.html

Используйте то место, которое находится под вашим контролем.

Кредиты:

  • Michaël Perrin - за предложение использовать w+ вместо r+
person zerkms    schedule 25.03.2011
comment
Извините, я должен был включить и эту ссылку (net.tutsplus.com/tutorials/other/…). У меня остается вопрос, я думаю, даже с flock, что будет, если скрипт умрет? - person Ben; 25.03.2011
comment
Т.е. если сценарий умирает между flock ($ fp, LOCK_EX) и flock ($ fp, LOCK_UN)? - person Ben; 25.03.2011
comment
@Ben: если скрипт умирает, блокировка, полученная flock, будет снята. - person zerkms; 25.03.2011
comment
Работает отлично. Однако я использую режим w+ для fopen на случай, если файл блокировки еще не существует. - person Michaël Perrin; 13.11.2013
comment
Согласно этому вопросу, использование fopen в режиме w+ может вызвать проблемы с использованием flock: stackoverflow.com/questions/13039065/ - person Nate; 20.01.2014
comment
@Nate: не уверен, что могу понять вашу точку зрения. w+ не имеет проблем при использовании с flock и ведет себя точно так, как ожидалось. - person zerkms; 20.01.2014
comment
В этом конкретном вопросе с использованием файла блокировки для предотвращения одновременного запуска нескольких сценариев w+ работает, но если у кого-то есть приложение, в котором есть содержимое в файле блокировки (например, время завершения работы последнего файла), w+ будет перезаписать содержимое. Это означает, что, поскольку fopen используется перед проверкой, заблокирован ли файл, содержимое файла блокировки будет удалено до того, как появится возможность его прочитать. Лучше использовать c+. Я прокомментировал это только потому, что я вроде как новичок, и мне потребовалось время, чтобы отладить эту проблему. - person Nate; 20.01.2014
comment
@Nate: ну, очевидно, что w+ перезапишет содержимое. Если просто копировать код, не понимая, как он работает - это их первая проблема :-) - person zerkms; 20.01.2014
comment
Однако что может быть не сразу очевидным, так это то, что он перезаписывает содержимое, даже если блокировка не может быть установлена, и я считаю, что использование c+ дает те же результаты, что и w+, за исключением того, что оно не создает этой проблемы. Надеюсь, если у кого-то возникнет эта проблема, они прочитают мои комментарии. - person Nate; 20.01.2014

В Symfony Framework вы можете использовать компонент блокировки symfony / lock

https://symfony.com/doc/current/console/lockable_trait.html

person Sebastian Viereck    schedule 15.01.2019

Я расширил концепцию от zerkms, чтобы создать функцию, которую можно вызывать из начала cron.

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

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

Я обнаружил, что, поскольку я часто использовал это, были случаи, когда я хотел заблокировать запуск других крон во время работы этого cron, поэтому я добавил дополнительный массив блокировок, которые являются другими именами блокировок для блокировки.

Затем были случаи, когда я хотел, чтобы этот cron запускался после того, как другие crons завершили работу, поэтому есть необязательный массив ожиданий блокировок, которые представляют собой другие имена блокировок для ожидания, пока ни один из них не будет запущен.

простой пример:

Cronlocker::CronLock('cron1', 'RunThis');
function RunThis() {
    echo('I ran!');
}

параметры обратного вызова и функции отказа:

Cronlocker::CronLock('cron2', 'RunThat', ['ran'], 'ImLocked');
function RunThat($x) {
    echo('I also ran! ' . $x);
}
function ImLocked($x) {
    echo('I am locked :-( ' . $x);
}

блокировка и ожидание:

Cronlocker::CronLock('cron3', 'RunAgain', null, null, ['cron1'], ['cron2']);
function RunAgain() {
    echo('I ran.<br />');
    echo('I block cron1 while I am running.<br />')
    echo('I wait for cron2 to finish if it is running.');
}

класс:

class Cronlocker {

    private static $LockFile = null;
    private static $LockFileBlocks = [];
    private static $LockFileWait = null;

    private static function GetLockfileName($lockname) {
        return "/tmp/lock-" . $lockname . ".txt";
    }

    /**
     * Locks a PHP script from being executed more than once at a time
     * @param string $lockname          Use a unique lock name for each lock that needs to be applied.
     * @param string $callback          The name of the function to call if the lock is OFF
     * @param array $callbackParams Optional array of parameters to apply to the callback function when called
     * @param string $callbackFail      Optional name of the function to call if the lock is ON
     * @param string[] $lockblocks      Optional array of locknames for other crons to also block while this cron is running
     * @param string[] $lockwaits       Optional array of locknames for other crons to wait until they finish running before this cron will run
     * @see http://stackoverflow.com/questions/5428631/php-preventing-collision-in-cron-file-lock-safe
     */
    public static function CronLock($lockname, $callback, $callbackParams = null, $callbackFail = null, $lockblocks = [], $lockwaits = []) {

        // check all the crons we are waiting for to finish running
        if (!empty($lockwaits)) {
            $waitingOnCron = true;
            while ($waitingOnCron) {
                $waitingOnCron = false;
                foreach ($lockwaits as $lockwait) {
                    self::$LockFileWait = null;
                    $tempfile = self::GetLockfileName($lockwait);
                    try {
                        self::$LockFileWait = fopen($tempfile, "w+");
                    } catch (Exception $e) {
                        //ignore error
                    }
                    if (flock(self::$LockFileWait, LOCK_EX | LOCK_NB)) { // do an exclusive lock
                        // cron we're waiting on isn't running
                        flock(self::$LockFileWait, LOCK_UN); // release the lock
                    } else {
                        // we're wating on a cron
                        $waitingOnCron = true;
                    }
                    if (is_resource(self::$LockFileWait))
                        fclose(self::$LockFileWait);
                    if ($waitingOnCron) break;      // no need to check any more
                }
                if ($waitingOnCron) sleep(15);      // wait a few seconds
            }
        }

        // block any additional crons from starting
        if (!empty($lockblocks)) {
            self::$LockFileBlocks = [];
            foreach ($lockblocks as $lockblock) {
                $tempfile = self::GetLockfileName($lockblock);
                try {
                    $block = fopen($tempfile, "w+");
                } catch (Exception $e) {
                    //ignore error
                }
                if (flock($block, LOCK_EX | LOCK_NB)) { // do an exclusive lock
                    // lock made
                    self::$LockFileBlocks[] = $block;
                } else {
                    // couldn't lock it, we ignore and move on
                }
            }
        }

        // set the cronlock
        self::$LockFile = null;
        $tempfile = self::GetLockfileName($lockname);
        $return = null;
        try {
            if (file_exists($tempfile) && !is_writable($tempfile)) {
                //assume we're hitting this from a browser and execute it regardless of the cronlock
                if (empty($callbackParams))
                    $return = $callback();
                else
                    $return = call_user_func_array($callback, $callbackParams);
            } else {
                self::$LockFile = fopen($tempfile, "w+");
            }
        } catch (Exception $e) {
            //ignore error
        }
        if (!empty(self::$LockFile)) {
            if (flock(self::$LockFile, LOCK_EX | LOCK_NB)) { // do an exclusive lock
                // do the work
                if (empty($callbackParams))
                    $return = $callback();
                else
                    $return = call_user_func_array($callback, $callbackParams);
                flock(self::$LockFile, LOCK_UN); // release the lock
            } else {
                // call the failed function
                if (!empty($callbackFail)) {
                    if (empty($callbackParams))
                        $return = $callbackFail();
                    else
                        $return = call_user_func_array($callbackFail, $callbackParams);
                }
            }
            if (is_resource(self::$LockFile))
                fclose(self::$LockFile);
        }

        // remove any lockblocks
        if (!empty($lockblocks)) {
            foreach (self::$LockFileBlocks as $LockFileBlock) {
                flock($LockFileBlock, LOCK_UN); // release the lock
                if (is_resource($LockFileBlock))
                    fclose($LockFileBlock);
            }
        }

        return $return;
    }

    /**
     * Releases the Cron Lock locking file, useful to specify on fatal errors
     */
    public static function ReleaseCronLock() {
        // release the cronlock
        if (!empty(self::$LockFile) && is_resource(self::$LockFile)) {
            var_dump('Cronlock released after error encountered: ' . self::$LockFile);
            flock(self::$LockFile, LOCK_UN);
            fclose(self::$LockFile);
        }
        // release any lockblocks too
        foreach (self::$LockFileBlocks as $LockFileBlock) {
            if (!empty($LockFileBlock) && is_resource($LockFileBlock)) {
                flock($LockFileBlock, LOCK_UN);
                fclose($LockFileBlock);
            }
        }
    }
}

Также следует реализовать на общей странице или встроить в существующий обработчик фатальных ошибок:

function fatal_handler() {
    // For cleaning up crons that fail
    Cronlocker::ReleaseCronLock();
}
register_shutdown_function("fatal_handler");
person Nico Westerdale    schedule 26.04.2018