Skip to content

Commit

Permalink
Merge pull request #272 from php-enqueue/fs-lock-interface
Browse files Browse the repository at this point in the history
[fs] Copy past Symfony's LockHandler (not awailable in Sf4).
  • Loading branch information
makasim committed Nov 19, 2017
2 parents b59e646 + 701617e commit ee04d72
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 67 deletions.
9 changes: 9 additions & 0 deletions pkg/fs/CannotObtainLockException.php
@@ -0,0 +1,9 @@
<?php

namespace Enqueue\Fs;

use Interop\Queue\Exception;

class CannotObtainLockException extends Exception
{
}
47 changes: 8 additions & 39 deletions pkg/fs/FsContext.php
Expand Up @@ -2,14 +2,12 @@

namespace Enqueue\Fs;

use Doctrine\ORM\Cache\Lock;
use Interop\Queue\InvalidDestinationException;
use Interop\Queue\PsrContext;
use Interop\Queue\PsrDestination;
use Interop\Queue\PsrQueue;
use Makasim\File\TempFile;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\LockHandler;

class FsContext implements PsrContext
{
Expand All @@ -29,14 +27,14 @@ class FsContext implements PsrContext
private $chmod;

/**
* @var LockHandler[]
* @var null
*/
private $lockHandlers;
private $pollingInterval;

/**
* @var null
* @var Lock
*/
private $pollingInterval;
private $lock;

/**
* @param string $storeDir
Expand All @@ -54,7 +52,7 @@ public function __construct($storeDir, $preFetchCount, $chmod, $pollingInterval
$this->chmod = $chmod;
$this->pollingInterval = $pollingInterval;

$this->lockHandlers = [];
$this->lock = new LegacyFilesystemLock();
}

/**
Expand Down Expand Up @@ -95,11 +93,6 @@ public function declareDestination(PsrDestination $destination)
InvalidDestinationException::assertDestinationInstanceOf($destination, FsDestination::class);

set_error_handler(function ($severity, $message, $file, $line) {
// do not throw on a deprecation notice.
if (E_USER_DEPRECATED === $severity && false !== strpos($message, LockHandler::class)) {
return;
}

throw new \ErrorException($message, 0, $severity, $file, $line);
});

Expand All @@ -123,20 +116,14 @@ public function workWithFile(FsDestination $destination, $mode, callable $callba

try {
$file = fopen($destination->getFileInfo(), $mode);
$lockHandler = $this->getLockHandler($destination);

if (false == $lockHandler->lock(true)) {
throw new \LogicException(sprintf('Cannot obtain the lock for destination %s', $destination->getName()));
}
$this->lock->lock($destination);

return call_user_func($callback, $destination, $file);
} finally {
if (isset($file)) {
fclose($file);
}
if (isset($lockHandler)) {
$lockHandler->release();
}
$this->lock->release($destination);

restore_error_handler();
}
Expand Down Expand Up @@ -184,11 +171,7 @@ public function createConsumer(PsrDestination $destination)

public function close()
{
foreach ($this->lockHandlers as $lockHandler) {
$lockHandler->release();
}

$this->lockHandlers = [];
$this->lock->releaseAll();
}

/**
Expand Down Expand Up @@ -234,18 +217,4 @@ private function getStoreDir()

return $this->storeDir;
}

/**
* @param FsDestination $destination
*
* @return LockHandler
*/
private function getLockHandler(FsDestination $destination)
{
if (false == isset($this->lockHandlers[$destination->getName()])) {
$this->lockHandlers[$destination->getName()] = new LockHandler($destination->getName(), $this->storeDir);
}

return $this->lockHandlers[$destination->getName()];
}
}
185 changes: 185 additions & 0 deletions pkg/fs/LegacyFilesystemLock.php
@@ -0,0 +1,185 @@
<?php

namespace Enqueue\Fs;

use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;

class LegacyFilesystemLock implements Lock
{
/**
* Array key is a destination name. String.
*
* @var LockHandler[]
*/
private $lockHandlers;

public function __construct()
{
$this->lockHandlers = [];
}

/**
* {@inheritdoc}
*/
public function lock(FsDestination $destination)
{
$lockHandler = $this->getLockHandler($destination);

if (false == $lockHandler->lock(true)) {
throw new CannotObtainLockException(sprintf('Cannot obtain the lock for destination %s', $destination->getName()));
}
}

/**
* {@inheritdoc}
*/
public function release(FsDestination $destination)
{
$lockHandler = $this->getLockHandler($destination);

$lockHandler->release();
}

public function releaseAll()
{
foreach ($this->lockHandlers as $lockHandler) {
$lockHandler->release();
}

$this->lockHandlers = [];
}

/**
* @param FsDestination $destination
*
* @return LockHandler
*/
private function getLockHandler(FsDestination $destination)
{
if (false == isset($this->lockHandlers[$destination->getName()])) {
$this->lockHandlers[$destination->getName()] = new LockHandler(
$destination->getName(),
$destination->getFileInfo()->getPath()
);
}

return $this->lockHandlers[$destination->getName()];
}
}

// symfony/lock component works only with 3.x and 4.x Symfony
// For symfony 2.x we should use LockHandler from symfony/component which was removed from 4.x
// because we cannot use both at the same time. I copied and pasted the lock handler here

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/**
* LockHandler class provides a simple abstraction to lock anything by means of
* a file lock.
*
* A locked file is created based on the lock name when calling lock(). Other
* lock handlers will not be able to lock the same name until it is released
* (explicitly by calling release() or implicitly when the instance holding the
* lock is destroyed).
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Romain Neutron <imprec@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*
* @deprecated since version 3.4, to be removed in 4.0. Use Symfony\Component\Lock\Store\SemaphoreStore or Symfony\Component\Lock\Store\FlockStore instead.
*/
class LockHandler
{
private $file;
private $handle;

/**
* @param string $name The lock name
* @param string|null $lockPath The directory to store the lock. Default values will use temporary directory
*
* @throws IOException If the lock directory could not be created or is not writable
*/
public function __construct($name, $lockPath = null)
{
$lockPath = $lockPath ?: sys_get_temp_dir();

if (!is_dir($lockPath)) {
$fs = new Filesystem();
$fs->mkdir($lockPath);
}

if (!is_writable($lockPath)) {
throw new IOException(sprintf('The directory "%s" is not writable.', $lockPath), 0, null, $lockPath);
}

$this->file = sprintf('%s/sf.%s.%s.lock', $lockPath, preg_replace('/[^a-z0-9\._-]+/i', '-', $name), hash('sha256', $name));
}

/**
* Lock the resource.
*
* @param bool $blocking Wait until the lock is released
*
* @throws IOException If the lock file could not be created or opened
*
* @return bool Returns true if the lock was acquired, false otherwise
*/
public function lock($blocking = false)
{
if ($this->handle) {
return true;
}

$error = null;

// Silence error reporting
set_error_handler(function ($errno, $msg) use (&$error) {
$error = $msg;
});

if (!$this->handle = fopen($this->file, 'r')) {
if ($this->handle = fopen($this->file, 'x')) {
chmod($this->file, 0444);
} elseif (!$this->handle = fopen($this->file, 'r')) {
usleep(100); // Give some time for chmod() to complete
$this->handle = fopen($this->file, 'r');
}
}
restore_error_handler();

if (!$this->handle) {
throw new IOException($error, 0, null, $this->file);
}

// On Windows, even if PHP doc says the contrary, LOCK_NB works, see
// https://bugs.php.net/54129
if (!flock($this->handle, LOCK_EX | ($blocking ? 0 : LOCK_NB))) {
fclose($this->handle);
$this->handle = null;

return false;
}

return true;
}

/**
* Release the resource.
*/
public function release()
{
if ($this->handle) {
flock($this->handle, LOCK_UN | LOCK_NB);
fclose($this->handle);
$this->handle = null;
}
}
}
23 changes: 23 additions & 0 deletions pkg/fs/Lock.php
@@ -0,0 +1,23 @@
<?php

namespace Enqueue\Fs;

interface Lock
{
/**
* Returns the control If the look has been obtained
* If not, should throw CannotObtainLockException exception.
*
* @param FsDestination $destination
*
* @throws CannotObtainLockException if look could not be obtained
*/
public function lock(FsDestination $destination);

/**
* @param FsDestination $destination
*/
public function release(FsDestination $destination);

public function releaseAll();
}
28 changes: 0 additions & 28 deletions pkg/fs/Tests/FsContextTest.php
Expand Up @@ -182,34 +182,6 @@ public function testShouldAllowPurgeMessagesFromQueue()
$this->assertEmpty(file_get_contents($tmpFile));
}

public function testShouldReleaseAllLocksOnClose()
{
new TempFile(sys_get_temp_dir().'/foo');
new TempFile(sys_get_temp_dir().'/bar');

$context = new FsContext(sys_get_temp_dir(), 1, 0666);

$fooQueue = $context->createQueue('foo');
$barQueue = $context->createTopic('bar');

$this->assertAttributeCount(0, 'lockHandlers', $context);

$context->workWithFile($fooQueue, 'r+', function () {
});
$context->workWithFile($barQueue, 'r+', function () {
});
$context->workWithFile($fooQueue, 'c+', function () {
});
$context->workWithFile($barQueue, 'c+', function () {
});

$this->assertAttributeCount(2, 'lockHandlers', $context);

$context->close();

$this->assertAttributeCount(0, 'lockHandlers', $context);
}

public function testShouldCreateFileOnFilesystemIfNotExistOnDeclareDestination()
{
$tmpFile = new TempFile(sys_get_temp_dir().'/'.uniqid());
Expand Down

0 comments on commit ee04d72

Please sign in to comment.