diff --git a/.travis.php.ini b/.travis.php.ini new file mode 100644 index 0000000..5121d16 --- /dev/null +++ b/.travis.php.ini @@ -0,0 +1,2 @@ +extension=memcache.so +extension=memcached.so diff --git a/.travis.yml b/.travis.yml index 3465114..9e0fdc6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,8 +17,7 @@ services: - redis-server before_script: - - ./tests/travis/memcache-setup.sh - - ./tests/travis/memcached-setup.sh + - if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then cat .travis.php.ini >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini ; echo "Loading additional config for version $TRAVIS_PHP_VERSION" ; fi - ./tests/travis/composer-setup.sh - ./tests/travis/mysql-setup.sh diff --git a/src/NinjaMutex/Lock/LockExpirationInterface.php b/src/NinjaMutex/Lock/LockExpirationInterface.php new file mode 100644 index 0000000..d169acb --- /dev/null +++ b/src/NinjaMutex/Lock/LockExpirationInterface.php @@ -0,0 +1,29 @@ +<?php +/** + * This file is part of ninja-mutex. + * + * (C) Kamil Dziedzic <arvenil@klecza.pl> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace NinjaMutex\Lock; + +/** + * Lock implementor + * + * @author Kamil Dziedzic <arvenil@klecza.pl> + */ +interface LockExpirationInterface +{ + /** + * @param int $expiration Expiration time of the lock in seconds. + */ + public function setExpiration($expiration); + + /** + * @param string $name + * @return bool + */ + public function clearLock($name); +} diff --git a/src/NinjaMutex/Lock/MemcacheLock.php b/src/NinjaMutex/Lock/MemcacheLock.php index f6bf998..b6b2f53 100644 --- a/src/NinjaMutex/Lock/MemcacheLock.php +++ b/src/NinjaMutex/Lock/MemcacheLock.php @@ -16,13 +16,107 @@ * * @author Kamil Dziedzic <arvenil@klecza.pl> */ -class MemcacheLock extends MemcacheLockAbstract +class MemcacheLock extends LockAbstract implements LockExpirationInterface { + /** + * Maximum expiration time in seconds (30 days) + * http://php.net/manual/en/memcache.add.php + */ + const MAX_EXPIRATION = 2592000; + + /** + * Memcache connection + * + * @var Memcache + */ + protected $memcache; + + /** + * @var int Expiration time of the lock in seconds + */ + protected $expiration = 0; + /** * @param Memcache $memcache */ public function __construct(Memcache $memcache) { - parent::__construct($memcache); + parent::__construct(); + + $this->memcache = $memcache; + } + + /** + * @param int $expiration Expiration time of the lock in seconds. If it's equal to zero (default), the lock will never expire. + * Max 2592000s (30 days), if greater it will be capped to 2592000 without throwing an error. + * WARNING: Using value higher than 0 may lead to race conditions. If you set too low expiration time + * e.g. 30s and critical section will run for 31s another process will gain lock at the same time, + * leading to unpredicted behaviour. Use with caution. + */ + public function setExpiration($expiration) + { + if ($expiration > static::MAX_EXPIRATION) { + $expiration = static::MAX_EXPIRATION; + } + $this->expiration = $expiration; + } + + /** + * Clear lock without releasing it + * Do not use this method unless you know what you do + * + * @param string $name name of lock + * @return bool + */ + public function clearLock($name) + { + if (!isset($this->locks[$name])) { + return false; + } + + unset($this->locks[$name]); + return true; + } + + /** + * @param string $name name of lock + * @param bool $blocking + * @return bool + */ + protected function getLock($name, $blocking) + { + if (!$this->memcache->add($name, serialize($this->getLockInformation()), 0, $this->expiration)) { + return false; + } + + return true; + } + + /** + * Release lock + * + * @param string $name name of lock + * @return bool + */ + public function releaseLock($name) + { + if (isset($this->locks[$name]) && $this->memcache->delete($name)) { + unset($this->locks[$name]); + + return true; + } + + return false; + } + + /** + * Check if lock is locked + * + * @param string $name name of lock + * @return bool + */ + public function isLocked($name) + { + return false !== $this->memcache->get($name); } } diff --git a/src/NinjaMutex/Lock/MemcacheLockAbstract.php b/src/NinjaMutex/Lock/MemcacheLockAbstract.php deleted file mode 100644 index 88a6a20..0000000 --- a/src/NinjaMutex/Lock/MemcacheLockAbstract.php +++ /dev/null @@ -1,77 +0,0 @@ -<?php -/** - * This file is part of ninja-mutex. - * - * (C) Kamil Dziedzic <arvenil@klecza.pl> - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace NinjaMutex\Lock; - -/** - * Abstract for lock implementor using Memcache or Memcached - * - * @author Kamil Dziedzic <arvenil@klecza.pl> - */ -abstract class MemcacheLockAbstract extends LockAbstract -{ - /** - * Memcache connection - * - * @var \Memcached|\Memcache - */ - protected $memcache; - - /** - * @param \Memcached|\Memcache $memcache - */ - public function __construct($memcache) - { - parent::__construct(); - - $this->memcache = $memcache; - } - - /** - * @param string $name - * @param bool $blocking - * @return bool - */ - protected function getLock($name, $blocking) - { - if (!$this->memcache->add($name, serialize($this->getLockInformation()))) { - return false; - } - - return true; - } - - /** - * Release lock - * - * @param string $name name of lock - * @return bool - */ - public function releaseLock($name) - { - if (isset($this->locks[$name]) && $this->memcache->delete($name)) { - unset($this->locks[$name]); - - return true; - } - - return false; - } - - /** - * Check if lock is locked - * - * @param string $name name of lock - * @return bool - */ - public function isLocked($name) - { - return false !== $this->memcache->get($name); - } -} diff --git a/src/NinjaMutex/Lock/MemcachedLock.php b/src/NinjaMutex/Lock/MemcachedLock.php index 639b449..88a2051 100644 --- a/src/NinjaMutex/Lock/MemcachedLock.php +++ b/src/NinjaMutex/Lock/MemcachedLock.php @@ -16,13 +16,108 @@ * * @author Kamil Dziedzic <arvenil@klecza.pl> */ -class MemcachedLock extends MemcacheLockAbstract +class MemcachedLock extends LockAbstract implements LockExpirationInterface { + /** + * Maximum expiration time in seconds (30 days) + * http://php.net/manual/en/memcached.add.php + */ + const MAX_EXPIRATION = 2592000; + + /** + * Memcache connection + * + * @var Memcached + */ + protected $memcached; + + /** + * @var int Expiration time of the lock in seconds + */ + protected $expiration = 0; + /** * @param Memcached $memcached */ - public function __construct(Memcached $memcached) + public function __construct($memcached) + { + parent::__construct(); + + $this->memcached = $memcached; + } + + /** + * @param int $expiration Expiration time of the lock in seconds. If it's equal to zero (default), the lock will never expire. + * Max 2592000s (30 days), if greater it will be capped to 2592000 without throwing an error. + * WARNING: Using value higher than 0 may lead to race conditions. If you set too low expiration time + * e.g. 30s and critical section will run for 31s another process will gain lock at the same time, + * leading to unpredicted behaviour. Use with caution. + */ + public function setExpiration($expiration) + { + if ($expiration > static::MAX_EXPIRATION) { + $expiration = static::MAX_EXPIRATION; + } + $this->expiration = $expiration; + } + + /** + * Clear lock without releasing it + * Do not use this method unless you know what you do + * + * @param string $name name of lock + * @return bool + */ + public function clearLock($name) { - parent::__construct($memcached); + if (!isset($this->locks[$name])) { + return false; + } + + unset($this->locks[$name]); + return true; + } + + /** + * @param string $name name of lock + * @param bool $blocking + * @return bool + */ + protected function getLock($name, $blocking) + { + if (!$this->memcached->add($name, serialize($this->getLockInformation()), $this->expiration)) { + return false; + } + + return true; + } + + /** + * Release lock + * + * @param string $name name of lock + * @return bool + */ + public function releaseLock($name) + { + if (isset($this->locks[$name]) && $this->memcached->delete($name)) { + unset($this->locks[$name]); + + return true; + } + + return false; + } + + /** + * Check if lock is locked + * + * @param string $name name of lock + * @return bool + */ + public function isLocked($name) + { + return false !== $this->memcached->get($name); } } + diff --git a/tests/NinjaMutex/AbstractTest.php b/tests/NinjaMutex/AbstractTest.php index 3bb0ec5..9b94711 100644 --- a/tests/NinjaMutex/AbstractTest.php +++ b/tests/NinjaMutex/AbstractTest.php @@ -9,12 +9,12 @@ */ namespace NinjaMutex; -use Memcache; -use Memcached; use NinjaMutex\Lock\FlockLock; use NinjaMutex\Lock\MemcacheLock; use NinjaMutex\Lock\MemcachedLock; use NinjaMutex\Lock\MySqlLock; +use NinjaMutex\Lock\Fabric\MemcacheLockFabric; +use NinjaMutex\Lock\Fabric\MemcachedLockFabric; use NinjaMutex\Mock\MockMemcache; use NinjaMutex\Mock\MockMemcached; use NinjaMutex\Mock\MockPredisClient; @@ -48,8 +48,14 @@ public function tearDown() rmdir('/tmp/mutex/'); } + /** + * @return array + */ public function lockImplementorProvider() { + $memcacheLockFabric = new MemcacheLockFabric(); + $memcachedLockFabric = new MemcachedLockFabric(); + $data = array( // Just mocks $this->provideFlockMockLock(), @@ -59,8 +65,8 @@ public function lockImplementorProvider() $this->providePredisRedisMockLock(), // Real locks $this->provideFlockLock(), - $this->provideMemcacheLock(), - $this->provideMemcachedLock(), + array($memcacheLockFabric->create()), + array($memcachedLockFabric->create()), $this->provideMysqlLock(), $this->providePredisRedisLock(), ); @@ -68,6 +74,9 @@ public function lockImplementorProvider() return $data; } + /** + * @return array + */ public function lockImplementorWithBackendProvider() { $data = array( @@ -80,6 +89,22 @@ public function lockImplementorWithBackendProvider() return $data; } + /** + * @return array + */ + public function lockFabricWithExpirationProvider() + { + $memcacheLockFabric = new MemcacheLockFabric(); + $memcachedLockFabric = new MemcachedLockFabric(); + + $data = array( + array($memcacheLockFabric), + array($memcachedLockFabric), + ); + + return $data; + } + /** * @return array */ @@ -126,28 +151,6 @@ protected function providePredisRedisMockLock() return array(new PredisRedisLock($predisMock), $predisMock); } - /** - * @return array - */ - protected function provideMemcacheLock() - { - $memcache = new Memcache(); - $memcache->connect('127.0.0.1', 11211); - - return array(new MemcacheLock($memcache)); - } - - /** - * @return array - */ - protected function provideMemcachedLock() - { - $memcached = new Memcached(); - $memcached->addServer('127.0.0.1', 11211); - - return array(new MemcachedLock($memcached)); - } - /** * @return array */ diff --git a/tests/NinjaMutex/Lock/Fabric/LockFabricWithExpirationInterface.php b/tests/NinjaMutex/Lock/Fabric/LockFabricWithExpirationInterface.php new file mode 100644 index 0000000..2ba96f2 --- /dev/null +++ b/tests/NinjaMutex/Lock/Fabric/LockFabricWithExpirationInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * This file is part of ninja-mutex. + * + * (C) Kamil Dziedzic <arvenil@klecza.pl> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace NinjaMutex\Lock\Fabric; +use NinjaMutex\Lock\LockInterface; +use NinjaMutex\Lock\LockExpirationInterface; + +/** + * Lock Fabric interface + * + * @author Kamil Dziedzic <arvenil@klecza.pl> + */ +interface LockFabricWithExpirationInterface +{ + /** + * @return LockInterface|LockExpirationInterface + */ + public function create(); +} diff --git a/tests/NinjaMutex/Lock/Fabric/MemcacheLockFabric.php b/tests/NinjaMutex/Lock/Fabric/MemcacheLockFabric.php new file mode 100644 index 0000000..1ceea5d --- /dev/null +++ b/tests/NinjaMutex/Lock/Fabric/MemcacheLockFabric.php @@ -0,0 +1,25 @@ +<?php +/** + * This file is part of ninja-mutex. + * + * (C) Kamil Dziedzic <arvenil@klecza.pl> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace NinjaMutex\Lock\Fabric; + +use Memcache; +use NinjaMutex\Lock\MemcacheLock; + +class MemcacheLockFabric implements LockFabricWithExpirationInterface { + /** + * @return MemcacheLock + */ + public function create() { + $memcache = new Memcache(); + $memcache->connect('127.0.0.1', 11211); + + return new MemcacheLock($memcache); + } +} diff --git a/tests/NinjaMutex/Lock/Fabric/MemcachedLockFabric.php b/tests/NinjaMutex/Lock/Fabric/MemcachedLockFabric.php new file mode 100644 index 0000000..11772c0 --- /dev/null +++ b/tests/NinjaMutex/Lock/Fabric/MemcachedLockFabric.php @@ -0,0 +1,25 @@ +<?php +/** + * This file is part of ninja-mutex. + * + * (C) Kamil Dziedzic <arvenil@klecza.pl> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace NinjaMutex\Lock\Fabric; + +use Memcached; +use NinjaMutex\Lock\MemcachedLock; + +class MemcachedLockFabric implements LockFabricWithExpirationInterface { + /** + * @return MemcachedLock + */ + public function create() { + $memcached = new Memcached(); + $memcached->addServer('127.0.0.1', 11211); + + return new MemcachedLock($memcached); + } +} diff --git a/tests/NinjaMutex/Lock/LockTest.php b/tests/NinjaMutex/Lock/LockTest.php index 5291449..5249c20 100644 --- a/tests/NinjaMutex/Lock/LockTest.php +++ b/tests/NinjaMutex/Lock/LockTest.php @@ -10,6 +10,7 @@ namespace NinjaMutex\Lock; use NinjaMutex\AbstractTest; +use NinjaMutex\Lock\Fabric\LockFabricWithExpirationInterface; use NinjaMutex\Mock\PermanentServiceInterface; use NinjaMutex\UnrecoverableMutexException; @@ -127,6 +128,8 @@ public function testIfLockIsReleasedAfterLockImplementorIsDestroyed(LockInterfac /** * @issue https://github.com/arvenil/ninja-mutex/pull/4 + * It's not working for hhvm, see below link to understand limitation + * https://github.com/facebook/hhvm/blob/af329776c9f740cc1c8c4791f673ba5aa49042ce/hphp/doc/inconsistencies#L40-L45 * * @dataProvider lockImplementorWithBackendProvider * @param LockInterface $lockImplementor @@ -160,4 +163,36 @@ public function testIfLockDestructorThrowsWhenBackendIsUnavailable(LockInterface $this->fail('An expected exception has not been raised.'); } + /** + * @issue https://github.com/arvenil/ninja-mutex/issues/12 + * @medium Timeout for test increased to ~5s http://stackoverflow.com/a/10535787/916440 + * + * @dataProvider lockFabricWithExpirationProvider + * @param LockFabricWithExpirationInterface $lockFabricWithExpiration + */ + public function testExpiration(LockFabricWithExpirationInterface $lockFabricWithExpiration) + { + $expiration = 2; // in seconds + $name = "lockWithExpiration_" . uniqid(); + $lockImplementor = $lockFabricWithExpiration->create(); + $lockImplementorWithExpiration = $lockFabricWithExpiration->create(); + $lockImplementorWithExpiration->setExpiration($expiration); + + // Aquire lock on implementor with lock expiration + $this->assertTrue($lockImplementorWithExpiration->acquireLock($name, 0)); + // We hope code was fast enough so $expiration time didn't pass yet and lock still should be held + $this->assertFalse($lockImplementor->acquireLock($name, 0)); + + // Let's wait for lock to expire + sleep($expiration); + + // Let's try again to lock + $this->assertTrue($lockImplementor->acquireLock($name, 0)); + + // Cleanup + $this->assertTrue($lockImplementor->releaseLock($name, 0)); + // Expired lock is unusable, we need to clean it's lock state or otherwise + // it will invoke in __destruct Exception (php*) or Fatal Error (hhvm) + $this->assertTrue($lockImplementorWithExpiration->clearLock($name, 0)); + } } diff --git a/tests/NinjaMutex/Mock/MockMemcached.php b/tests/NinjaMutex/Mock/MockMemcached.php index 77f6f27..b6c02d7 100644 --- a/tests/NinjaMutex/Mock/MockMemcached.php +++ b/tests/NinjaMutex/Mock/MockMemcached.php @@ -9,14 +9,13 @@ */ namespace NinjaMutex\Mock; -use Memcached; /** * Mock memcached to mimic mutex functionality * * @author Kamil Dziedzic <arvenil@klecza.pl> */ -class MockMemcached extends Memcached implements PermanentServiceInterface +class MockMemcached implements PermanentServiceInterface { /** * @var string[] @@ -34,12 +33,13 @@ public function __construct() } /** - * @param string $key - * @param mixed $value - * @param null $expiration + * @param string $key + * @param mixed $value + * @param int|null $expiration + * @param null $udf_flags * @return bool */ - public function add($key, $value, $expiration = null) + public function add($key, $value, $expiration = null, &$udf_flags = null) { if (!$this->available) { return false; @@ -58,9 +58,10 @@ public function add($key, $value, $expiration = null) * @param string $key * @param null $cache_cb * @param null $cas_token + * @param null $udf_flags * @return bool|mixed|string */ - public function get($key, $cache_cb = null, &$cas_token = null) + public function get($key, $cache_cb = null, &$cas_token = null, &$udf_flags = null) { if (!$this->available) { return false; diff --git a/tests/travis/composer-setup.sh b/tests/travis/composer-setup.sh index 10a6a39..08d5476 100755 --- a/tests/travis/composer-setup.sh +++ b/tests/travis/composer-setup.sh @@ -1,3 +1,3 @@ #!/bin/sh -wget -nc http://getcomposer.org/composer.phar && php composer.phar install --dev --prefer-source +wget -nc http://getcomposer.org/composer.phar && php composer.phar install --prefer-source diff --git a/tests/travis/memcache-setup.sh b/tests/travis/memcache-setup.sh deleted file mode 100755 index b0e419f..0000000 --- a/tests/travis/memcache-setup.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -install_memcache() { - if [ $(expr "${TRAVIS_PHP_VERSION}" "!=" "hhvm") -eq 1 ] && [ $(expr "${TRAVIS_PHP_VERSION}" "!=" "hhvm-nightly") -eq 1 ]; then - echo "extension=memcache.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - fi - - return $? -} - -install_memcache > ~/memcache.log || ( echo "=== MEMCACHE INSTALL FAILED ==="; cat ~/memcache.log; exit 1 ) diff --git a/tests/travis/memcached-setup.sh b/tests/travis/memcached-setup.sh deleted file mode 100755 index 14c9905..0000000 --- a/tests/travis/memcached-setup.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -install_memcached() { - if [ $(expr "${TRAVIS_PHP_VERSION}" "!=" "hhvm") -eq 1 ] && [ $(expr "${TRAVIS_PHP_VERSION}" "!=" "hhvm-nightly") -eq 1 ]; then - echo "extension=memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - fi - - return $? -} - -install_memcached > ~/memcached.log || ( echo "=== MEMCACHED INSTALL FAILED ==="; cat ~/memcached.log; exit 1 )