Skip to content

Commit ba72c91

Browse files
author
Willem Stuursma-Ruwen
authored
Merge pull request #18 from php-lock/mysql-lock
Add locking backend for MySQL GET_LOCK() function
2 parents 741eb27 + 941e058 commit ba72c91

File tree

7 files changed

+124
-21
lines changed

7 files changed

+124
-21
lines changed

classes/mutex/MySQLMutex.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace malkusch\lock\mutex;
4+
5+
use malkusch\lock\exception\LockAcquireException;
6+
use malkusch\lock\exception\TimeoutException;
7+
8+
class MySQLMutex extends LockMutex
9+
{
10+
/**
11+
* @var \PDO
12+
*/
13+
private $pdo;
14+
15+
/**
16+
* @var string
17+
*/
18+
private $name;
19+
/**
20+
* @var int
21+
*/
22+
private $timeout;
23+
24+
public function __construct(\PDO $PDO, $name, $timeout = 0)
25+
{
26+
$this->pdo = $PDO;
27+
28+
if (\strlen($name) > 64) {
29+
throw new \InvalidArgumentException("The maximum length of the lock name is 64 characters.");
30+
}
31+
32+
$this->name = $name;
33+
$this->timeout = $timeout;
34+
}
35+
36+
/**
37+
* @throws LockAcquireException
38+
*/
39+
public function lock()
40+
{
41+
$statement = $this->pdo->prepare("SELECT GET_LOCK(?,?)");
42+
43+
$statement->execute([
44+
$this->name,
45+
$this->timeout,
46+
]);
47+
48+
$statement->setFetchMode(\PDO::FETCH_NUM);
49+
$row = $statement->fetch();
50+
51+
if ($row[0] == 1) {
52+
/*
53+
* Returns 1 if the lock was obtained successfully.
54+
*/
55+
return;
56+
}
57+
58+
if ($row[0] === null) {
59+
/*
60+
* NULL if an error occurred (such as running out of memory or the thread was killed with mysqladmin kill).
61+
*/
62+
throw new LockAcquireException("An error occurred while acquiring the lock");
63+
}
64+
65+
throw new TimeoutException("Timeout when acquiring lock.");
66+
}
67+
68+
public function unlock()
69+
{
70+
$statement = $this->pdo->prepare("DO RELEASE_LOCK(?)");
71+
$statement->execute([
72+
$this->name
73+
]);
74+
}
75+
}

classes/util/Loop.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function end()
5050
}
5151

5252
/**
53-
* Repeats executing a code until it was succesful.
53+
* Repeats executing a code until it was successful.
5454
*
5555
* The code has to be designed in a way that it can be repeated without any
5656
* side effects. When execution was successful it should notify that event
@@ -84,7 +84,7 @@ public function execute(callable $code)
8484
/*
8585
* Calculate max time remaining, don't sleep any longer than that.
8686
*/
87-
$usecRemaining = intval(($timeout - microtime(true)) * 1e6);
87+
$usecRemaining = \intval(($timeout - microtime(true)) * 1e6);
8888

8989
if ($usecRemaining <= 0) {
9090
/*
@@ -93,7 +93,7 @@ public function execute(callable $code)
9393
throw new TimeoutException("Timeout of $this->timeout seconds exceeded.");
9494
}
9595

96-
$usleep = min($usecRemaining, \random_int($min, $max));
96+
$usleep = \min($usecRemaining, \random_int($min, $max));
9797

9898
usleep($usleep);
9999
}

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
},
2929
"require-dev": {
3030
"ext-memcached": "*",
31-
"ext-redis": "^2.2.4|^3.0|^4.0",
31+
"ext-redis": "*",
32+
"ext-pcntl": "*",
3233
"ext-pdo_mysql": "*",
3334
"ext-pdo_sqlite": "*",
3435
"kriswallsmith/spork": "^0.3",

tests/mutex/MemcachedMutexTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function testFailAcquireLock()
3737
{
3838
$mutex = new MemcachedMutex("testFailAcquireLock", $this->memcached, 1);
3939

40-
$this->memcached->add(MemcachedMutex::PREFIX."testFailAcquireLock", true, 2);
40+
$this->memcached->add(MemcachedMutex::PREFIX."testFailAcquireLock", "xxx", 999);
4141

4242
$mutex->synchronized(function () {
4343
$this->fail("execution is not expected");

tests/mutex/MutexConcurrencyTest.php

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,11 @@
2525
*/
2626
class MutexConcurrencyTest extends \PHPUnit_Framework_TestCase
2727
{
28-
2928
/**
3029
* @var \PDO The pdo instance.
3130
*/
3231
private $pdo;
33-
32+
3433
/**
3534
* Gets a PDO instance.
3635
*
@@ -57,9 +56,13 @@ private function getPDO($dsn, $user)
5756
private function fork($concurrency, callable $code)
5857
{
5958
$manager = new ProcessManager();
59+
$manager->setDebug(true);
60+
6061
for ($i = 0; $i < $concurrency; $i++) {
6162
$manager->fork($code);
6263
}
64+
65+
$manager->check();
6366
}
6467

6568
/**
@@ -99,21 +102,22 @@ public function provideTestHighContention()
99102
{
100103
$cases = array_map(function (array $mutexFactory) {
101104
$file = tmpfile();
102-
fputs($file, pack("i", 0));
103-
fflush($file);
105+
fwrite($file, pack("i", 0));
104106

105107
return [
106108
function ($increment) use ($file) {
107-
fseek($file, 0);
109+
rewind($file);
110+
flock($file, LOCK_EX);
108111
$data = fread($file, 4);
109112
$counter = unpack("i", $data)[1];
110113

111114
$counter += $increment;
112115

113-
fseek($file, 0);
116+
rewind($file);
114117
fwrite($file, pack("i", $counter));
115-
fflush($file);
116-
118+
119+
flock($file, LOCK_UN);
120+
117121
return $counter;
118122
},
119123
$mutexFactory[0]
@@ -122,15 +126,16 @@ function ($increment) use ($file) {
122126

123127
$addPDO = function ($dsn, $user, $vendor) use (&$cases) {
124128
$pdo = $this->getPDO($dsn, $user);
125-
$pdo->beginTransaction();
126-
129+
127130
$options = ["mysql" => "engine=InnoDB"];
128131
$option = isset($options[$vendor]) ? $options[$vendor] : "";
129132
$pdo->exec("CREATE TABLE IF NOT EXISTS counter(id INT PRIMARY KEY, counter INT) $option");
130-
133+
134+
$pdo->beginTransaction();
131135
$pdo->exec("DELETE FROM counter");
132136
$pdo->exec("INSERT INTO counter VALUES (1, 0)");
133137
$pdo->commit();
138+
134139
$this->pdo = null;
135140

136141
$cases[$vendor] = [
@@ -259,6 +264,15 @@ function ($uri) {
259264
return new PHPRedisMutex($apis, "test", $timeout);
260265
}];
261266
}
267+
268+
if (getenv("MYSQL_DSN")) {
269+
$cases["MySQLMutex"] = [function ($timeout = 3) {
270+
$pdo = new \PDO(getenv("MYSQL_DSN"), getenv("MYSQL_USER"));
271+
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
272+
273+
return new MySQLMutex($pdo, "test", $timeout);
274+
}];
275+
}
262276

263277
return $cases;
264278
}

tests/mutex/MutexTest.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@
2323
*/
2424
class MutexTest extends \PHPUnit_Framework_TestCase
2525
{
26-
27-
const TIMEOUT = 3;
26+
const TIMEOUT = 4;
2827

2928
/**
3029
* Provides Mutex factories.
@@ -109,6 +108,15 @@ function ($uri) {
109108
}];
110109
}
111110

111+
if (getenv("MYSQL_DSN")) {
112+
$cases["MySQLMutex"] = [function () {
113+
$pdo = new \PDO(getenv("MYSQL_DSN"), getenv("MYSQL_USER"));
114+
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
115+
116+
return new MySQLMutex($pdo, "test", self::TIMEOUT);
117+
}];
118+
}
119+
112120
return $cases;
113121
}
114122

@@ -150,11 +158,12 @@ public function testRelease(callable $mutexFactory)
150158
* @param callable $mutexFactory The Mutex factory.
151159
* @test
152160
* @dataProvider provideMutexFactories
153-
* @requires PHP 7.0
154161
*/
155162
public function testLiveness(callable $mutexFactory)
156163
{
157164
$manager = new ProcessManager();
165+
$manager->setDebug(true);
166+
158167
$manager->fork(function () use ($mutexFactory) {
159168
$mutex = call_user_func($mutexFactory);
160169
$mutex->synchronized(function () {
@@ -168,6 +177,8 @@ public function testLiveness(callable $mutexFactory)
168177
$mutex = call_user_func($mutexFactory);
169178
$mutex->synchronized(function () {
170179
});
180+
181+
$manager->check();
171182
}
172183

173184
/**
@@ -177,7 +188,6 @@ public function testLiveness(callable $mutexFactory)
177188
* @test
178189
* @dataProvider provideMutexFactories
179190
* @expectedException \DomainException
180-
* @requires PHP 5.6
181191
*/
182192
public function testSynchronizedPassesExceptionThrough(callable $mutexFactory)
183193
{

tests/mutex/PHPRedisMutexTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace malkusch\lock\mutex;
44

55
use Redis;
6-
use RedisException;
76

87
/**
98
* Tests for PHPRedisMutex.
@@ -93,6 +92,10 @@ public function testSyncronizedWorks($serialization)
9392

9493
public function dpSerializationModes()
9594
{
95+
if (!class_exists(Redis::class)) {
96+
return [];
97+
}
98+
9699
$serializers = [
97100
[Redis::SERIALIZER_NONE],
98101
[Redis::SERIALIZER_PHP],

0 commit comments

Comments
 (0)