PHP Lesson 3: File locking in PHP without flock(), but with NFS support!
File locks without flock() (but with NFS support)
I wanted to discuss a file lock class I've developed based on experience with that subject the last few years. I've found it to be particularly useful, especially because it works without flock(), and on NFS shared network drives. It does
not use flock(), since that's not (very well) supported on NFS filesystems, even though it's sometimes claimed otherwise. It is not very portable outside the Unix/Linux environment, however.
The pro's:
- lean and mean
- works on local hard-drives and network drives
- no flock()
- decent NFS support
- can be used across multiple (PHP) processes and even multiple servers, as long as they share the path where the lock files are written
The con's:
- limited Windows support (PHP.net says: now available on Windows platforms (Vista, Server 2008 or greater).)
- only useable when the same code is used all application that should acknowledge the lock, i.e. not useable for locking against other application (e.g. system tools)
Inner workings
My file lock class uses a name to uniquely reference a lock. So all applications that should acknowledge the lock, should all use the same name. For example, if you have four applications that need to get mutual access to some resource named "directory", all applications should use this file lock class and specify "directory" as the name for the lock. That way they all work against the same lock file on the filesystem.
It is not required that the applications live inside the same process, or even the same PC. As long as they use the same code, and have access to the same file lock on a (shared) filesystem, it works. This is very convenient when you have to obtain access to a resource that might be in use over multiple servers.
The way the file lock class works is quite simple. There is a fixed path where lock files are stored. The name of the lock is used to determine the filename on the filesystem. So e.g. if the lock path is "/tmp/", and the lock is named "directory", the lock file will be "/tmp/directory.lock". Then, when you try to lock the lock, the implementation writes a unique temporary file in the lock path, and tries to hardlink it on filesystem level to the the actual lock filename. This might require some more detail:
When locking lock "directory", the following filename is generated, where PIDOFPROCESS is replaced with the current PID of the process, and RANDOMNUMBER is the result of a call to mt_rand():
/tmp/directory.lock.pid.PIDOFPROCESS.mtrand.RANDOMNUMBER
Then, this file is hardlinked to the actual lock file, so when it succeeds, you have two files referencing the same inode on disk:
/tmp/directory.lock.pid.PIDOFPROCESS.mtrand.RANDOMNUMBER
/tmp/directory.lock
Then, regardless of the success of the link() call, the unique file is removed, which in filesystem terms means the referencecount to the inode is lowered: the actual file is not deleted, since /tmp/directory.lock still references it!
When the link() call succeeded, we obtained the lock. When it failed, the file already existed, meaning some other process got the lock, and a failure to lock is reported.
The trick in the entire setup is the fact that link() is guaranteed to be "thread safe" on local filesystems, but also on NFS systems! It's a guaranteed atomic operation. Simply creating the original file may seem like a simpler solution, but it will fail not work, since (weirdly enough) creating a file is NOT an atomic operation!
One note: ofcourse "/tmp/" is only useful when all processes that require locking functionality live on the same PC. When you need a shared NFS directory as a lockpath, don't forget to update the lockpath in the code! (As detailed below.)
The interface
define( 'LOCKFILE_SUCCESS', 1 ); // success result: the lock was obtained!
define( 'LOCKFILE_ERROR_ALREADY_LOCKED', -1 ); // error result: lock was already locked, try again later
define( 'LOCKFILE_ERROR_FAIL_TO_OBTAIN_LOCK', -2 ); // error result: oops, lock wasn't locked, but failed to write lockfile!
class Filelock {
public static $lockpath = '/tmp/';
private $name;
private $filename;
private $i_locked = false;
public function __construct( $name );
public function is_locked(); // always instantly returns with either TRUE or FALSE to indicate if SOMEONE (not us per se!) has the lock
public function try_lock(); // attempts to lock, returns instantly when already locked returning FALSE, or returns instantly when locked, returning TRUE
public function lock(); // will wait inifitely to obtain a lock
public function unlock( $force=false ); // when called without arguments, unlocks ONLY if this object also obtained the lock... when true is passed as first parameter, ALWAYS forcibly unlocks!
private function bake_data(); // private function
}
To use this lock file class, simply create an instance of the Filelock class with the name of the lock (as explained earlier) :
$lock = new Filelock( "myfirstlock" );
if ($lock->is_locked())
die( "Process locked!" );
if ($lock->try_lock())
die( "First time lock call succeeded, we have exclusive access!" );
$lock->lock(); // will wait infinitely to obtain the lock
... do work, not being disturbed by anyone ...
$lock->unlock(); // release!
To change the lockpath, simply set the static variable once:
Filelock::$lockpath = '/var/lib/locks';
The implementation
Copy/paste from below, or download
here.
define( 'LOCKFILE_SUCCESS', 1 );
define( 'LOCKFILE_ERROR_ALREADY_LOCKED', -1 );
define( 'LOCKFILE_ERROR_FAIL_TO_OBTAIN_LOCK', -2 );
class Filelock {
public static $lockpath = '/tmp/';
private $name;
private $filename;
private $i_locked = false;
public function __construct( $name ) {
$this->name = $name;
$this->filename = Filelock::$lockpath . preg_replace( '/\W/', '_', $name ) . '.lock';
}
public function is_locked() {
return file_exists( $this->filename );
}
public function try_lock() {
if ($this->is_locked())
return LOCKFILE_ERROR_ALREADY_LOCKED;
// write tempfile
$tempfile = $this->filename . '.pid.' . getmypid() . '.mtrand.' . mt_rand();
if (!@file_put_contents( $tempfile, serialize( $this->bake_data() ) ))
return LOCKFILE_ERROR_FAILED_OBTAINING_LOCK;
// try to hard link tempfile to lockpath
if (!@link( $tempfile, $this->filename )) {
@unlink( $tempfile );
return LOCKFILE_ERROR_ALREADY_LOCKED;
}
// cleanup
$this->i_locked = true;
@unlink( $tempfile );
return LOCKFILE_SUCCESS;
}
public function lock() {
do {
$res = $this->try_lock();
if ($res != LOCKFILE_ERROR_ALREADY_LOCKED)
break;
usleep( 100000 ); // try again after 0.1 seconds
} while (true);
return $res;
}
public function unlock( $force=false ) {
if ($force || $this->i_locked) {
$this->i_locked = false;
@unlink( $this->filename );
}
}
public function bake_data() {
return array(
'mypid' => getmypid(),
'timestamp' => date( 'd-m-Y H:i:s' ),
'name' => $this->name,
);
}
}
8 comment(s)
Click to write your own comment
Unfortunately the lock will not be removed if there is an exception in the executed code (at: "do work, not being disturbed by anyone"). So it's locked and never get unlocked?