diff --git a/.travis.yml b/.travis.yml index 36b2236d..04611c69 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,8 @@ env: services: - memcached + - mysql + - postgresql - redis-server matrix: @@ -36,13 +38,13 @@ before_install: - redis-server --port 63791 & install: - - composer require --dev squizlabs/php_codesniffer + - composer install --no-scripts --no-suggest --no-interaction before_script: - mysql -e 'create database test;' - psql -c 'create database test;' -U postgres script: - - vendor/bin/phpunit + - vendor/bin/phpunit --debug - vendor/bin/phpcs --standard=PSR2 classes/ tests/ diff --git a/classes/mutex/PgAdvisoryLockMutex.php b/classes/mutex/PgAdvisoryLockMutex.php new file mode 100644 index 00000000..8580ee9f --- /dev/null +++ b/classes/mutex/PgAdvisoryLockMutex.php @@ -0,0 +1,62 @@ +pdo = $PDO; + + $hashed_name = hash("sha256", $name, true); + + if (false === $hashed_name) { + throw new \RuntimeException("Unable to hash the key, sha256 algorithm is not supported."); + } + + list($bytes1, $bytes2) = str_split($hashed_name, 4); + + $this->key1 = unpack("i", $bytes1)[1]; + $this->key2 = unpack("i", $bytes2)[1]; + } + + public function lock() + { + $statement = $this->pdo->prepare("SELECT pg_advisory_lock(?,?)"); + + $statement->execute([ + $this->key1, + $this->key2, + ]); + } + + public function unlock() + { + $statement = $this->pdo->prepare("SELECT pg_advisory_unlock(?,?)"); + $statement->execute([ + $this->key1, + $this->key2 + ]); + } +} diff --git a/classes/util/Loop.php b/classes/util/Loop.php index db33adae..2e696a6d 100644 --- a/classes/util/Loop.php +++ b/classes/util/Loop.php @@ -78,7 +78,7 @@ public function execute(callable $code) break; } - $min = $minWait * 2 ** $i; + $min = (int) $minWait * 1.5 ** $i; $max = $min * 2; /* diff --git a/composer.json b/composer.json index fdc3fedb..56932074 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,9 @@ "autoload": { "psr-4": {"malkusch\\lock\\": "classes/"} }, + "config": { + "sort-packages": true + }, "require": { "php": ">=5.6", "psr/log": "^1", @@ -28,18 +31,23 @@ }, "require-dev": { "ext-memcached": "*", - "ext-redis": "*", "ext-pcntl": "*", "ext-pdo_mysql": "*", "ext-pdo_sqlite": "*", + "ext-redis": "*", + "johnkary/phpunit-speedtrap": "^1.0", "kriswallsmith/spork": "^0.3", "mikey179/vfsStream": "^1.5.0", - "phpunit/phpunit": "^5", "php-mock/php-mock-phpunit": "^1", + "phpunit/phpunit": "^5", "predis/predis": "~1.0", + "squizlabs/php_codesniffer": "^3.2", "zetacomponents/system-information": "~1.1" }, "archive": { "exclude": ["/tests"] + }, + "scripts": { + "fix-cs": "vendor/bin/phpcbf --standard=PSR2 classes/ tests/" } } diff --git a/phpunit.xml b/phpunit.xml index f1c1dd18..1434ae7f 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -5,4 +5,7 @@ tests + + + \ No newline at end of file diff --git a/tests/mutex/MutexConcurrencyTest.php b/tests/mutex/MutexConcurrencyTest.php index 3b9e32a2..23de5b0a 100644 --- a/tests/mutex/MutexConcurrencyTest.php +++ b/tests/mutex/MutexConcurrencyTest.php @@ -30,6 +30,20 @@ class MutexConcurrencyTest extends \PHPUnit_Framework_TestCase */ private $pdo; + /** + * @var string + */ + private $path; + + protected function tearDown() + { + if ($this->path) { + unlink($this->path); + } + + parent::tearDown(); + } + /** * Gets a PDO instance. * @@ -102,13 +116,16 @@ public function provideTestHighContention() { $cases = array_map(function (array $mutexFactory) { $file = tmpfile(); - fwrite($file, pack("i", 0)); + $this->assertEquals(4, fwrite($file, pack("i", 0)), "Expected 4 bytes to be written to temporary file."); return [ function ($increment) use ($file) { rewind($file); flock($file, LOCK_EX); $data = fread($file, 4); + + $this->assertEquals(4, strlen($data), "Expected four bytes to be present in temporary file."); + $counter = unpack("i", $data)[1]; $counter += $increment; @@ -209,16 +226,16 @@ public function testSerialisation(callable $mutexFactory) */ public function provideMutexFactories() { - $path = stream_get_meta_data(tmpfile())["uri"]; - + $this->path = tempnam(sys_get_temp_dir(), "mutex-concurrency-test"); + $cases = [ - "flock" => [function ($timeout = 3) use ($path) { - $file = fopen($path, "w"); + "flock" => [function ($timeout = 3) { + $file = fopen($this->path, "w"); return new FlockMutex($file); }], - "semaphore" => [function ($timeout = 3) use ($path) { - $semaphore = sem_get(ftok($path, "b")); + "semaphore" => [function ($timeout = 3) { + $semaphore = sem_get(ftok($this->path, "b")); $this->assertTrue(is_resource($semaphore)); return new SemaphoreMutex($semaphore); }], @@ -273,6 +290,15 @@ function ($uri) { return new MySQLMutex($pdo, "test", $timeout); }]; } + + if (getenv("PGSQL_DSN")) { + $cases["PgAdvisoryLockMutex"] = [function () { + $pdo = new \PDO(getenv("PGSQL_DSN"), getenv("PGSQL_USER")); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + + return new PgAdvisoryLockMutex($pdo, "test"); + }]; + } return $cases; } diff --git a/tests/mutex/MutexTest.php b/tests/mutex/MutexTest.php index 1c378115..72ab22b6 100644 --- a/tests/mutex/MutexTest.php +++ b/tests/mutex/MutexTest.php @@ -113,7 +113,16 @@ function ($uri) { $pdo = new \PDO(getenv("MYSQL_DSN"), getenv("MYSQL_USER")); $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); - return new MySQLMutex($pdo, "test", self::TIMEOUT); + return new MySQLMutex($pdo, "test" . time(), self::TIMEOUT); + }]; + } + + if (getenv("PGSQL_DSN")) { + $cases["PgAdvisoryLockMutex"] = [function () { + $pdo = new \PDO(getenv("PGSQL_DSN"), getenv("PGSQL_USER")); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + + return new PgAdvisoryLockMutex($pdo, "test"); }]; } @@ -151,36 +160,7 @@ public function testRelease(callable $mutexFactory) $mutex->synchronized(function () { }); } - - /** - * Tests that locks will be released automatically. - * - * @param callable $mutexFactory The Mutex factory. - * @test - * @dataProvider provideMutexFactories - */ - public function testLiveness(callable $mutexFactory) - { - $manager = new ProcessManager(); - $manager->setDebug(true); - - $manager->fork(function () use ($mutexFactory) { - $mutex = call_user_func($mutexFactory); - $mutex->synchronized(function () { - exit; - }); - }); - $manager->wait(); - - sleep(self::TIMEOUT - 1); - - $mutex = call_user_func($mutexFactory); - $mutex->synchronized(function () { - }); - $manager->check(); - } - /** * Tests synchronized() rethrows the exception of the code. * diff --git a/tests/mutex/PgAdvisoryLockMutexTest.php b/tests/mutex/PgAdvisoryLockMutexTest.php new file mode 100644 index 00000000..5155f0da --- /dev/null +++ b/tests/mutex/PgAdvisoryLockMutexTest.php @@ -0,0 +1,95 @@ + + * @link bitcoin:1P5FAZ4QhXCuwYPnLZdk3PJsqePbu1UDDA Donations + * @license WTFPL + */ +class PgAdvisoryLockMutexTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PDO|\PHPUnit_Framework_MockObject_MockObject + */ + private $pdo; + + /** + * @var PgAdvisoryLockMutex + */ + private $mutex; + + protected function setUp() + { + parent::setUp(); + + $this->pdo = $this->createMock(\PDO::class); + + $this->mutex = new PgAdvisoryLockMutex($this->pdo, "test" . uniqid()); + } + + public function testAcquireLock() + { + $statement = $this->createMock(\PDOStatement::class); + + $this->pdo->expects($this->once()) + ->method("prepare") + ->with("SELECT pg_advisory_lock(?,?)") + ->willReturn($statement); + + $statement->expects($this->once()) + ->method("execute") + ->with( + $this->logicalAnd( + $this->isType("array"), + $this->countOf(2), + $this->callback(function (...$arguments) { + $integers = $arguments[0]; + + foreach ($integers as $each) { + $this->assertInternalType("integer", $each); + } + + return true; + }) + ) + ); + + $this->mutex->lock(); + } + + public function testReleaseLock() + { + $statement = $this->createMock(\PDOStatement::class); + + $this->pdo->expects($this->once()) + ->method("prepare") + ->with("SELECT pg_advisory_unlock(?,?)") + ->willReturn($statement); + + $statement->expects($this->once()) + ->method("execute") + ->with( + $this->logicalAnd( + $this->isType("array"), + $this->countOf(2), + $this->callback(function (...$arguments) { + $integers = $arguments[0]; + + foreach ($integers as $each) { + $this->assertLessThan(1 << 32, $each); + $this->assertGreaterThan(-(1 << 32), $each); + $this->assertInternalType("integer", $each); + } + + return true; + }) + ) + ); + + $this->mutex->unlock(); + } +} diff --git a/tests/mutex/PredisMutexTest.php b/tests/mutex/PredisMutexTest.php index aaf6bc0e..a45b71ae 100644 --- a/tests/mutex/PredisMutexTest.php +++ b/tests/mutex/PredisMutexTest.php @@ -12,11 +12,11 @@ * * REDIS_URIS - a comma separated list of redis:// URIs. * - * @author Markus Malkusch - * @link bitcoin:1P5FAZ4QhXCuwYPnLZdk3PJsqePbu1UDDA Donations + * @author Markus Malkusch + * @link bitcoin:1P5FAZ4QhXCuwYPnLZdk3PJsqePbu1UDDA Donations * @license WTFPL - * @see PredisMutex - * @group redis + * @see PredisMutex + * @group redis */ class PredisMutexTest extends \PHPUnit_Framework_TestCase { @@ -28,10 +28,17 @@ class PredisMutexTest extends \PHPUnit_Framework_TestCase protected function setUp() { parent::setUp(); - - $this->client = new Client($this->getPredisConfig()); - if (count($this->getPredisConfig()) == 1) { + $config = $this->getPredisConfig(); + + if (null === $config) { + $this->markTestSkipped(); + return; + } + + $this->client = new Client($config); + + if (count($config) === 1) { $this->client->flushall(); // Clear any existing locks } } @@ -44,16 +51,19 @@ private function getPredisConfig() $servers = explode(",", getenv("REDIS_URIS")); - return array_map(function ($redisUri) { - return str_replace("redis://", "tcp://", $redisUri); - }, $servers); + return array_map( + function ($redisUri) { + return str_replace("redis://", "tcp://", $redisUri); + }, + $servers + ); } /** * Tests add() fails. * * @test - * @expectedException \malkusch\lock\exception\LockAcquireException + * @expectedException \malkusch\lock\exception\LockAcquireException * @expectedExceptionCode \malkusch\lock\exception\MutexException::REDIS_NOT_ENOUGH_SERVERS */ public function testAddFails() @@ -61,10 +71,12 @@ public function testAddFails() $client = new Client("redis://127.0.0.1:12345"); $mutex = new PredisMutex([$client], "test"); - - $mutex->synchronized(function () { - $this->fail("Code execution is not expected"); - }); + + $mutex->synchronized( + function () { + $this->fail("Code execution is not expected"); + } + ); } /**