PHP - Предотвращение столкновений в Cron - Безопасная блокировка файлов? - PullRequest
15 голосов
/ 25 марта 2011

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

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

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

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

Ответы [ 3 ]

27 голосов
/ 25 марта 2011

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

$fp = fopen(sys_get_temp_dir().DIRECTORY_SEPARATOR."lock.txt", "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);

Кредиты:

1 голос
/ 15 января 2019

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

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

0 голосов
/ 26 апреля 2018

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

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

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

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

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

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

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 /5317950/php-predotvraschenie-stolknovenii-v-cron-bezopasnaya-blokirovka-failov
     */
    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");
...