Skip to content

Instantly share code, notes, and snippets.

@craiga
Last active December 25, 2015 11:48
Show Gist options
  • Save craiga/6971226 to your computer and use it in GitHub Desktop.
Save craiga/6971226 to your computer and use it in GitHub Desktop.
Locking functions
<?php
require_once(realpath(dirname(__FILE__ )) . "/../logging-trait/Logging.php");
/**
* Functions for locking.
*
* _lock, _unlock and _isLockedByOther for file-based, non-blocking locking.
*
* @author Craig Anderson <craiga@craiga.id.au>
* @link https://gist.github.com/craiga/6971226
*/
trait Locking
{
use Logging; // include https://gist.github.com/craiga/1849563
protected function _isLockedByOther()
{
$isLocked = false;
if (!$this->_ignoreLock && !$this->_ownsLock) {
$lockFileName = $this->_getLockFileName();
if (file_exists($lockFileName)) {
$timeCreated = filemtime($lockFileName);
$timeSinceCreation = time() - $timeCreated;
if ($timeSinceCreation > $this->_lockTimeout) {
$this->_log(LOG_NOTICE, "Lock file has existed for %s seconds; timing out.", number_format($timeSinceCreation));
if (!@unlink($this->_getLockFileName())) {
throw new RuntimeException(sprintf("Couldn't delete lock file %s", $this->_getLockFileName()));
}
}
else {
$isLocked = true;
}
}
}
return $isLocked;
}
protected function _lock()
{
if ($this->_isLockedByOther()) {
throw new RuntimeException("Cannot lock this process as a lock belonging to another instance already exists!");
}
if ($this->_lockDepth < 1) {
$this->_log(LOG_DEBUG, "Locking");
}
else {
$this->_log(LOG_DEBUG, "Renewing lock");
}
if (!@touch($this->_getLockFileName())) {
throw new RuntimeException(sprintf("Couldn't create lock file %s", $this->_getLockFileName()));
}
$this->_ownsLock = true;
$this->_lockDepth++;
}
protected function _unlock()
{
if ($this->_isLockedByOther()) {
throw new RuntimeException("Cannot unlock this process as a lock belonging to another instance already exists!");
}
$this->_lockDepth--;
if ($this->_lockDepth < 1) {
$this->_log(LOG_DEBUG, "Unlocking");
if (!@unlink($this->_getLockFileName())) {
throw new RuntimeException(sprintf("Couldn't delete lock file %s", $this->_getLockFileName()));
}
$this->_ownsLock = false;
}
}
protected function _getLockFileName()
{
$f = sys_get_temp_dir() . DIRECTORY_SEPARATOR . get_class($this) . ".lock";
$this->_log(LOG_DEBUG, "Using lock file %s", $f);
return $f;
}
protected $_lockDepth = 0;
protected $_ownsLock = false;
protected $_lockTimeout = 3600; // one hour
protected $_ignoreLock = false;
}
<?php
class LockingExample
{
use Locking;
function doStuff() {
// configure locking
$this->_lockTimeout = 60 * 5; // five minutes
if ($this->_isLockedByOther()) { // check for lock
$this->_log(LOG_INFO, "Process locked; aborting."); // die
}
else {
$this->_lock(); // lock
try {
$items = $this->_getItems();
foreach ($items as $item) {
$this->_lock(); // renew lock
try {
$item->doMoreStuff();
$this->_unlock(); // decrease lock depth
}
catch (Exception $e) {
$this->_unlock(); // decrease lock depth in error
throw $e;
}
}
$this->_unlock(); // unlock
}
catch (Exception $e) {
$this->_unlock(); // unlock in error
throw $e;
}
}
}
}
<?php
require("Locking.php");
class LockingTest
{
use Locking;
public function __construct()
{
$this->setLogging(false);
}
public function setLogging($enable = true)
{
$this->_logFile = $enable ? "php://output" : null;
}
public function lock()
{
return $this->_lock();
}
public function unlock()
{
return $this->_unlock();
}
public function isLockedByOther()
{
return $this->_isLockedByOther();
}
public function setIgnoreLock($ignore = true)
{
$this->_ignoreLock = $ignore;
}
}
function assertEqual($expected, $actual) {
$failCount = 0;
if ($expected != $actual) {
printf("FAIL: Expected \"%s\"; got \"%s\"\n", $expected, $actual);
$failCount++;
}
return $failCount;
}
$failCount = 0;
$a = new LockingTest();
$b = new LockingTest();
print("Testing initial state\n");
$failCount += assertEqual(false, $a->isLockedByOther());
$failCount += assertEqual(false, $b->isLockedByOther());
$a->lock();
print("Testing after lock\n");
$failCount += assertEqual(false, $a->isLockedByOther());
$failCount += assertEqual(true, $b->isLockedByOther());
print("Testing after ignoring lock\n");
$b->setIgnoreLock(true);
$failCount += assertEqual(false, $a->isLockedByOther());
$failCount += assertEqual(false, $b->isLockedByOther());
$b->setIgnoreLock(false);
print("Testing after reignoring lock\n");
$failCount += assertEqual(false, $a->isLockedByOther());
$failCount += assertEqual(true, $b->isLockedByOther());
$a->unlock();
print("Testing after unlock\n");
$failCount += assertEqual(false, $a->isLockedByOther());
$failCount += assertEqual(false, $b->isLockedByOther());
$a->lock();
print("Testing locking from seperate instance after lock\n");
try {
$b->lock();
printf("FAIL: Expected exception\n");
$failCount++;
}
catch (Exception $e) {
// expected
}
print("Testing unlocking from seperate instance after lock\n");
try {
$b->unlock();
printf("FAIL: Expected exception\n");
$failCount++;
}
catch (Exception $e) {
// expected
}
printf("Testing still locked\n");
$failCount += assertEqual(false, $a->isLockedByOther());
$failCount += assertEqual(true, $b->isLockedByOther());
$a->unlock();
$b->lock();
$b->lock();
$b->lock();
printf("Testing after three locks\n");
$failCount += assertEqual(true, $a->isLockedByOther());
$failCount += assertEqual(false, $b->isLockedByOther());
$b->unlock();
printf("Testing after three locks and one unlock\n");
$failCount += assertEqual(true, $a->isLockedByOther());
$failCount += assertEqual(false, $b->isLockedByOther());
$b->unlock();
printf("Testing after three locks and two unlocks\n");
$failCount += assertEqual(true, $a->isLockedByOther());
$failCount += assertEqual(false, $b->isLockedByOther());
$b->unlock();
printf("Testing after three locks and three unlocks\n");
$failCount += assertEqual(false, $a->isLockedByOther());
$failCount += assertEqual(false, $b->isLockedByOther());
printf("%s %s failed.\n", number_format($failCount), $failCount == 1 ? "test" : "tests");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment