symphony/lib/toolkit/class.mutex.php
<?php
/**
* @package toolkit
*/
/**
* The Mutex class is a crude locking class that generates files
* with a specific time to live. It has basic functions to create a
* lock, release a lock or refresh a lock.
*/
class Mutex
{
/**
* An associative array of files that have been locked by the Mutex
* class, with the key the filename, and the values an associative array
* with `time` and `ttl` values.
* @var array
*/
private static $lockFiles;
/**
* Creates a lock file if one does not already exist with a certain
* time to live (TTL) at a specific path. If a lock already exists,
* false will be returned otherwise boolean depending if a lock
* file was created successfully or not.
*
* @param string $id
* The name of the lock file, which gets obfuscated using
* generateLockFileName.
* @param integer $ttl
* The length, in seconds, that the lock should exist for. Defaults
* to 5.
* @param string $path
* The path the lock should be written, defaults to the current
* working directory
* @return boolean
*/
public static function acquire($id, $ttl = 5, $path = '.')
{
$lockFile = self::__generateLockFileName($id, $path);
// If this thread already has acquired the lock, return true.
if (isset(self::$lockFiles[$lockFile])) {
$age = time() - self::$lockFiles[$lockFile]['time'];
return ($age < $ttl ? false : true);
}
// Disable log temporarily because we actually depend on fopen()
// failing with E_WARNING here and we do not want Symphony to throw
// errors or spam logfiles.
try {
ErrorHandler::$logDisabled = true;
$lock = fopen($lockFile, 'xb');
ErrorHandler::$logDisabled = false;
self::$lockFiles[$lockFile] = array('time' => time(), 'ttl' => $ttl);
fclose($lock);
return true;
} catch (Exception $ex) {
// If, for some reason, lock file was not unlinked before,
// remove it if it is old enough.
if (file_exists($lockFile)) {
$age = time() - filemtime($lockFile);
if ($age > $ttl) {
unlink($lockFile);
}
}
// Return false anyway - just in case two or more threads
// do the same check and unlink at the same time.
return false;
}
}
/**
* Removes a lock file. This is the only way a lock file can be removed
*
* @param string $id
* The original name of the lock file (note that this will be different from
* the name of the file saved on the file system)
* @param string $path
* The path to the lock, defaults to the current working directory
* @return boolean
*/
public static function release($id, $path = '.')
{
$lockFile = self::__generateLockFileName($id, $path);
if (!empty(self::$lockFiles[$lockFile])) {
unset(self::$lockFiles[$lockFile]);
return General::deleteFile($lockFile, false);
}
return false;
}
/**
* Updates a lock file to 'keep alive' for another 'x' seconds.
*
* @param string $id
* The name of the lock file, which gets obfuscated using
* `__generateLockFileName()`.
* @param integer $ttl
* The length, in seconds, that the lock should be extended by.
* Defaults to 5.
* @param string $path
* The path to the lock, defaults to the current working directory
* @return boolean
*/
public static function refresh($id, $ttl = 5, $path = '.')
{
return touch(self::__generateLockFileName($id, $path), time() + $ttl, time());
}
/**
* Checks if a lock exists, purely on the presence on the lock file.
* This function takes the unobfuscated lock name
* Others should not depend on value returned by this function,
* because by the time it returns, the lock file can be created or deleted
* by another thread.
*
* @since Symphony 2.2
* @param string $id
* The name of the lock file, which gets obfuscated using
* generateLockFileName.
* @param string $path
* The path the lock should be written, defaults to the current
* working directory
* @return boolean
*/
public static function lockExists($id, $path)
{
$lockFile = self::__generateLockFileName($id, $path);
return file_exists($lockFile);
}
/**
* Generates a lock filename using an MD5 hash of the `$id` and
* `$path`. Lock files are given a .lock extension
*
* @param string $id
* The name of the lock file to be obfuscated
* @param string $path
* The path the lock should be written
* @return string
*/
private static function __generateLockFileName($id, $path = null)
{
// This function is called from all others, so it is a good point to initialize Mutex handling.
if (!is_array(self::$lockFiles)) {
self::$lockFiles = array();
register_shutdown_function(array(__CLASS__, '__shutdownCleanup'));
}
if (is_null($path)) {
$path = sys_get_temp_dir();
}
// Use realpath, because shutdown function may operate in different working directory.
// So we need to be sure that path is absolute.
return rtrim(realpath($path), '/') . '/' . md5($id) . '.lock';
}
/**
* Releases all locks on expired files.
*
* @since Symphony 2.2.2
*/
public static function __shutdownCleanup()
{
$now = time();
if (is_array(self::$lockFiles)) {
foreach (self::$lockFiles as $lockFile => $meta) {
if (($now - $meta['time'] > $meta['ttl']) && file_exists($lockFile)) {
unlink($lockFile);
}
}
}
}
}