Skip to content

Commit c79c8df

Browse files
Merge branch 'pgadvisory'
2 parents df19397 + 51dae5f commit c79c8df

9 files changed

+245
-57
lines changed

.travis.yml

+4-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ env:
2424

2525
services:
2626
- memcached
27+
- mysql
28+
- postgresql
2729
- redis-server
2830

2931
matrix:
@@ -36,13 +38,13 @@ before_install:
3638
- redis-server --port 63791 &
3739

3840
install:
39-
- composer require --dev squizlabs/php_codesniffer
41+
- composer install --no-scripts --no-suggest --no-interaction
4042

4143
before_script:
4244
- mysql -e 'create database test;'
4345
- psql -c 'create database test;' -U postgres
4446

4547
script:
46-
- vendor/bin/phpunit
48+
- vendor/bin/phpunit --debug
4749
- vendor/bin/phpcs --standard=PSR2 classes/ tests/
4850

classes/mutex/PgAdvisoryLockMutex.php

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace malkusch\lock\mutex;
4+
5+
use malkusch\lock\exception\LockAcquireException;
6+
use malkusch\lock\exception\TimeoutException;
7+
8+
class PgAdvisoryLockMutex extends LockMutex
9+
{
10+
/**
11+
* @var \PDO
12+
*/
13+
private $pdo;
14+
15+
/**
16+
* @var int
17+
*/
18+
private $key1;
19+
20+
/**
21+
* @var int
22+
*/
23+
private $key2;
24+
25+
/**
26+
* @throws \RuntimeException
27+
*/
28+
public function __construct(\PDO $PDO, $name)
29+
{
30+
$this->pdo = $PDO;
31+
32+
$hashed_name = hash("sha256", $name, true);
33+
34+
if (false === $hashed_name) {
35+
throw new \RuntimeException("Unable to hash the key, sha256 algorithm is not supported.");
36+
}
37+
38+
list($bytes1, $bytes2) = str_split($hashed_name, 4);
39+
40+
$this->key1 = unpack("i", $bytes1)[1];
41+
$this->key2 = unpack("i", $bytes2)[1];
42+
}
43+
44+
public function lock()
45+
{
46+
$statement = $this->pdo->prepare("SELECT pg_advisory_lock(?,?)");
47+
48+
$statement->execute([
49+
$this->key1,
50+
$this->key2,
51+
]);
52+
}
53+
54+
public function unlock()
55+
{
56+
$statement = $this->pdo->prepare("SELECT pg_advisory_unlock(?,?)");
57+
$statement->execute([
58+
$this->key1,
59+
$this->key2
60+
]);
61+
}
62+
}

classes/util/Loop.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public function execute(callable $code)
7878
break;
7979
}
8080

81-
$min = $minWait * 2 ** $i;
81+
$min = (int) $minWait * 1.5 ** $i;
8282
$max = $min * 2;
8383

8484
/*

composer.json

+10-2
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,33 @@
2121
"autoload": {
2222
"psr-4": {"malkusch\\lock\\": "classes/"}
2323
},
24+
"config": {
25+
"sort-packages": true
26+
},
2427
"require": {
2528
"php": ">=5.6",
2629
"psr/log": "^1",
2730
"paragonie/random_compat": "^1|^2"
2831
},
2932
"require-dev": {
3033
"ext-memcached": "*",
31-
"ext-redis": "*",
3234
"ext-pcntl": "*",
3335
"ext-pdo_mysql": "*",
3436
"ext-pdo_sqlite": "*",
37+
"ext-redis": "*",
38+
"johnkary/phpunit-speedtrap": "^1.0",
3539
"kriswallsmith/spork": "^0.3",
3640
"mikey179/vfsStream": "^1.5.0",
37-
"phpunit/phpunit": "^5",
3841
"php-mock/php-mock-phpunit": "^1",
42+
"phpunit/phpunit": "^5",
3943
"predis/predis": "~1.0",
44+
"squizlabs/php_codesniffer": "^3.2",
4045
"zetacomponents/system-information": "~1.1"
4146
},
4247
"archive": {
4348
"exclude": ["/tests"]
49+
},
50+
"scripts": {
51+
"fix-cs": "vendor/bin/phpcbf --standard=PSR2 classes/ tests/"
4452
}
4553
}

phpunit.xml

+3
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@
55
<testsuite>
66
<directory>tests</directory>
77
</testsuite>
8+
<listeners>
9+
<listener class="JohnKary\PHPUnit\Listener\SpeedTrapListener" />
10+
</listeners>
811
</phpunit>

tests/mutex/MutexConcurrencyTest.php

+33-7
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ class MutexConcurrencyTest extends \PHPUnit_Framework_TestCase
3030
*/
3131
private $pdo;
3232

33+
/**
34+
* @var string
35+
*/
36+
private $path;
37+
38+
protected function tearDown()
39+
{
40+
if ($this->path) {
41+
unlink($this->path);
42+
}
43+
44+
parent::tearDown();
45+
}
46+
3347
/**
3448
* Gets a PDO instance.
3549
*
@@ -102,13 +116,16 @@ public function provideTestHighContention()
102116
{
103117
$cases = array_map(function (array $mutexFactory) {
104118
$file = tmpfile();
105-
fwrite($file, pack("i", 0));
119+
$this->assertEquals(4, fwrite($file, pack("i", 0)), "Expected 4 bytes to be written to temporary file.");
106120

107121
return [
108122
function ($increment) use ($file) {
109123
rewind($file);
110124
flock($file, LOCK_EX);
111125
$data = fread($file, 4);
126+
127+
$this->assertEquals(4, strlen($data), "Expected four bytes to be present in temporary file.");
128+
112129
$counter = unpack("i", $data)[1];
113130

114131
$counter += $increment;
@@ -209,16 +226,16 @@ public function testSerialisation(callable $mutexFactory)
209226
*/
210227
public function provideMutexFactories()
211228
{
212-
$path = stream_get_meta_data(tmpfile())["uri"];
213-
229+
$this->path = tempnam(sys_get_temp_dir(), "mutex-concurrency-test");
230+
214231
$cases = [
215-
"flock" => [function ($timeout = 3) use ($path) {
216-
$file = fopen($path, "w");
232+
"flock" => [function ($timeout = 3) {
233+
$file = fopen($this->path, "w");
217234
return new FlockMutex($file);
218235
}],
219236

220-
"semaphore" => [function ($timeout = 3) use ($path) {
221-
$semaphore = sem_get(ftok($path, "b"));
237+
"semaphore" => [function ($timeout = 3) {
238+
$semaphore = sem_get(ftok($this->path, "b"));
222239
$this->assertTrue(is_resource($semaphore));
223240
return new SemaphoreMutex($semaphore);
224241
}],
@@ -273,6 +290,15 @@ function ($uri) {
273290
return new MySQLMutex($pdo, "test", $timeout);
274291
}];
275292
}
293+
294+
if (getenv("PGSQL_DSN")) {
295+
$cases["PgAdvisoryLockMutex"] = [function () {
296+
$pdo = new \PDO(getenv("PGSQL_DSN"), getenv("PGSQL_USER"));
297+
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
298+
299+
return new PgAdvisoryLockMutex($pdo, "test");
300+
}];
301+
}
276302

277303
return $cases;
278304
}

tests/mutex/MutexTest.php

+10-30
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,16 @@ function ($uri) {
113113
$pdo = new \PDO(getenv("MYSQL_DSN"), getenv("MYSQL_USER"));
114114
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
115115

116-
return new MySQLMutex($pdo, "test", self::TIMEOUT);
116+
return new MySQLMutex($pdo, "test" . time(), self::TIMEOUT);
117+
}];
118+
}
119+
120+
if (getenv("PGSQL_DSN")) {
121+
$cases["PgAdvisoryLockMutex"] = [function () {
122+
$pdo = new \PDO(getenv("PGSQL_DSN"), getenv("PGSQL_USER"));
123+
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
124+
125+
return new PgAdvisoryLockMutex($pdo, "test");
117126
}];
118127
}
119128

@@ -151,36 +160,7 @@ public function testRelease(callable $mutexFactory)
151160
$mutex->synchronized(function () {
152161
});
153162
}
154-
155-
/**
156-
* Tests that locks will be released automatically.
157-
*
158-
* @param callable $mutexFactory The Mutex factory.
159-
* @test
160-
* @dataProvider provideMutexFactories
161-
*/
162-
public function testLiveness(callable $mutexFactory)
163-
{
164-
$manager = new ProcessManager();
165-
$manager->setDebug(true);
166-
167-
$manager->fork(function () use ($mutexFactory) {
168-
$mutex = call_user_func($mutexFactory);
169-
$mutex->synchronized(function () {
170-
exit;
171-
});
172-
});
173-
$manager->wait();
174-
175-
sleep(self::TIMEOUT - 1);
176-
177-
$mutex = call_user_func($mutexFactory);
178-
$mutex->synchronized(function () {
179-
});
180163

181-
$manager->check();
182-
}
183-
184164
/**
185165
* Tests synchronized() rethrows the exception of the code.
186166
*
+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
namespace malkusch\lock\mutex;
4+
5+
use malkusch\lock\exception\LockAcquireException;
6+
use malkusch\lock\exception\LockReleaseException;
7+
8+
/**
9+
* @author Willem Stuursma-Ruwen <[email protected]>
10+
* @link bitcoin:1P5FAZ4QhXCuwYPnLZdk3PJsqePbu1UDDA Donations
11+
* @license WTFPL
12+
*/
13+
class PgAdvisoryLockMutexTest extends \PHPUnit_Framework_TestCase
14+
{
15+
/**
16+
* @var \PDO|\PHPUnit_Framework_MockObject_MockObject
17+
*/
18+
private $pdo;
19+
20+
/**
21+
* @var PgAdvisoryLockMutex
22+
*/
23+
private $mutex;
24+
25+
protected function setUp()
26+
{
27+
parent::setUp();
28+
29+
$this->pdo = $this->createMock(\PDO::class);
30+
31+
$this->mutex = new PgAdvisoryLockMutex($this->pdo, "test" . uniqid());
32+
}
33+
34+
public function testAcquireLock()
35+
{
36+
$statement = $this->createMock(\PDOStatement::class);
37+
38+
$this->pdo->expects($this->once())
39+
->method("prepare")
40+
->with("SELECT pg_advisory_lock(?,?)")
41+
->willReturn($statement);
42+
43+
$statement->expects($this->once())
44+
->method("execute")
45+
->with(
46+
$this->logicalAnd(
47+
$this->isType("array"),
48+
$this->countOf(2),
49+
$this->callback(function (...$arguments) {
50+
$integers = $arguments[0];
51+
52+
foreach ($integers as $each) {
53+
$this->assertInternalType("integer", $each);
54+
}
55+
56+
return true;
57+
})
58+
)
59+
);
60+
61+
$this->mutex->lock();
62+
}
63+
64+
public function testReleaseLock()
65+
{
66+
$statement = $this->createMock(\PDOStatement::class);
67+
68+
$this->pdo->expects($this->once())
69+
->method("prepare")
70+
->with("SELECT pg_advisory_unlock(?,?)")
71+
->willReturn($statement);
72+
73+
$statement->expects($this->once())
74+
->method("execute")
75+
->with(
76+
$this->logicalAnd(
77+
$this->isType("array"),
78+
$this->countOf(2),
79+
$this->callback(function (...$arguments) {
80+
$integers = $arguments[0];
81+
82+
foreach ($integers as $each) {
83+
$this->assertLessThan(1 << 32, $each);
84+
$this->assertGreaterThan(-(1 << 32), $each);
85+
$this->assertInternalType("integer", $each);
86+
}
87+
88+
return true;
89+
})
90+
)
91+
);
92+
93+
$this->mutex->unlock();
94+
}
95+
}

0 commit comments

Comments
 (0)