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

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

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

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

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

Ответ 1

Этот образец был взят по адресу 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);

Кредиты:

Ответ 3

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

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

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

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

Затем были случаи, когда я хотел, чтобы этот 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 http://stackoverflow.com/info/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");