PHP - Предотвращение столкновений в Cron - Безопасная блокировка файлов?
Я пытаюсь найти безопасный способ предотвратить столкновение задания cron (т.е. предотвратить его запуск, если другой экземпляр уже запущен).
Некоторые варианты, которые я нашел, рекомендуют использовать блокировку файла.
Это действительно безопасный вариант? Что произойдет, если сценарий умрет, например? Замок останется?
Есть ли другие способы сделать это?
2 ответа
Этот образец был взят по адресу 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);
Кредиты:
- Michaël Perrin - за предложение использовать
w+
вместоr+
В Symfony Framework вы можете использовать компонент блокировки Symfony/ Lock
Я расширил концепцию 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://stackru.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");