이 세상에 하나는 남기고 가자

세상에 필요한 소스코드 한줄 남기고 가자

PHP에서 symlink()를 이용해 lock 구현하기

아사마루

PHP에서 외부 Extensions 없이 간단히 lock을 구현하고자 할 때 file을 이용하는 경우가 많다(file에 대한 읽기/쓰기 lock을 말하는 것이 아니다).

예를들어 어떤 프로세스가 중첩되어 실행되는 것을 막기 위한 lock이 필요할 수 있다. 나의 경우는 LaravelQueue를 사용하면서 Daemon을 동시에 여러개를 띄운 상황에서 특정 작업의 중첩을 막기 위해 필요했다. Job에 대한 중복 실행은 Laravel의 Queue가 DB lock을 이용해 자체적으로 처리한다. 하지만 나는 Job의 중복 실행을 막는 것이 아닌 프로세스의 중첩을 막는 것이 필요했다.

가장 간단한 구현을 생각하면 아래와 같이 할 수 있다.

#!/usr/bin/php
<?php
define('LOCK_FILE', "/var/run/" . basename($argv[0], ".php") . ".lock");
if (isLocked()) {
    die("Already running.\n");
}

# The rest of your script goes here....
echo "Hello world!\n";
sleep(30);

unlink(LOCK_FILE);
exit(0);

function isLocked()
{
    # If lock file exists, check if stale.  If exists and is not stale, return TRUE
    # Else, create lock file and return FALSE.

    if (file_exists(LOCK_FILE)) {
        # check if it's stale
        $lockingPID = trim(file_get_contents(LOCK_FILE));

        # Get all active PIDs.
        $pids = explode("\n", trim(`ps -e | awk '{print $1}'`));

        # If PID is still active, return true
        if (in_array($lockingPID, $pids)) {
            return true;
        }

        # Lock-file is stale, so kill it.  Then move on to re-creating it.
        echo "Removing stale lock file.\n";
        unlink(LOCK_FILE);
    }

    file_put_contents(LOCK_FILE, getmypid() . "\n");
    return false;

}

위 코드는 Kevin Traas가 제시한 코드다. 내가 laravel에서 구현 했던 코드는 예시로 사용하기엔 여러 가지 다른 코드가 섞여있어 이해하기 쉬운 코드로 예를 들었다. 어차피 원리는 거의 유사하다.

위 코드는 잘 동작할 것처럼 보이지만 그렇지 않다(일반적인 상황에서는 잘 동작한다). 이유는 lock을 검사하는 코드와 lock을 생성하는 코드 사이에 다른 프로세스가 끼어들 수 있기 때문이다.

위에서 이야기한 것처럼 Daemon이 동시에 여러개 떠 있는 상황에서 동시에 lock을 사용하다보면 생각보다 빈번하게 오작동 한다(프로세스가 중첩된다).

이 문제를 해결하기 위해서는 symlink()를 이용하는 방법이 있다.

아래는 Kevin Traas가 제시한 코드의 문제와 해결 방법을 제시한 Radu Cristescu의 코드다.

#!/usr/bin/php
<?php

define('LOCK_FILE', "/var/run/" . basename($argv[0], ".php") . ".lock");

if (!tryLock()) {
    die("Already running.\n");
}

# remove the lock on exit (Control+C doesn't count as 'exit'?)
register_shutdown_function('unlink', LOCK_FILE);

# The rest of your script goes here....
echo "Hello world!\n";
sleep(30);

exit(0);

function tryLock()
{
    # If lock file exists, check if stale.  If exists and is not stale, return TRUE
    # Else, create lock file and return FALSE.

    if (@symlink("/proc/" . getmypid(), LOCK_FILE) !== false) # the @ in front of 'symlink' is to suppress the NOTICE you get if the LOCK_FILE exists
    {
        return true;
    }

    # link already exists
    # check if it's stale
    if (is_link(LOCK_FILE) && !is_dir(LOCK_FILE)) {
        unlink(LOCK_FILE);
        # try to lock again
        return tryLock();
    }

    return false;
}

간단히 설명하자면 symlink()를 이용해서 lock이 걸려 있는지를 검사하는 과정과 lock을 생성하는 과정을 한번에 처리하는 것이다. 이 방법을 사용하면 보다 안전하게 lock-file 매커니즘을 사용할 수 있다.

Comment