Skip to content

Commit f934b95

Browse files
committed
[Lock] Add MysqlStore that use GET_LOCK
* Key is hashed with sha256 to ensure it stays between 1 and 64 characters * Create a new PDO connection for each lock, to avoid multiple locks on the same name in the same session
1 parent 07766b3 commit f934b95

File tree

4 files changed

+170
-0
lines changed

4 files changed

+170
-0
lines changed

src/Symfony/Component/Lock/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
4.1.0
5+
-----
6+
7+
* added Mysql store using GET_LOCK
8+
49
3.4.0
510
-----
611

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Lock\Store;
13+
14+
use Symfony\Component\Lock\Exception\LockConflictedException;
15+
use Symfony\Component\Lock\Key;
16+
use Symfony\Component\Lock\StoreInterface;
17+
18+
/**
19+
* MysqlStore is a StoreInterface implementation using MySQL/MariaDB GET_LOCK function
20+
*
21+
* @author Jérôme TAMARELLE <jerome@tamarelle.net>
22+
*/
23+
class MysqlStore implements StoreInterface
24+
{
25+
private $dsn;
26+
private $username;
27+
private $password;
28+
private $options;
29+
30+
private $waitTimeout;
31+
32+
/**
33+
* @param array $connection
34+
* - db_dsn:
35+
* - db_username:
36+
* - db_password:
37+
* - db_options:
38+
* - timeout: Time in seconds to wait for a lock to be released.
39+
* A negative timeout value means infinite timeout.
40+
*/
41+
public function __construct(array $options)
42+
{
43+
$this->dsn = $options['dsn'] ?? 'mysql:host=localhost';
44+
$this->username = $options['db_username'] ?? 'root';
45+
$this->password = $options['db_password'] ?? null;
46+
$this->options = $options['db_options'] ?? array();
47+
$this->waitTimeout = $options['timeout'] ?? -1;
48+
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*/
53+
public function save(Key $key)
54+
{
55+
$this->lock($key, false);
56+
}
57+
58+
/**
59+
* {@inheritdoc}
60+
*/
61+
public function waitAndSave(Key $key)
62+
{
63+
$this->lock($key, true);
64+
}
65+
66+
private function lock(Key $key, bool $blocking)
67+
{
68+
// The lock is maybe already acquired.
69+
if ($key->hasState(__CLASS__)) {
70+
return;
71+
}
72+
73+
// no timeout for impatient
74+
$timeout = $blocking ? $this->waitTimeout : 0;
75+
76+
$connection = new \PDO($this->dsn, $this->username, $this->password, $this->options);
77+
$connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
78+
79+
$stmt = $connection->prepare('SELECT GET_LOCK(:key, :timeout)');
80+
$stmt->bindValue(':key', hash('sha256', $key), \PDO::PARAM_STR);
81+
$stmt->bindValue(':timeout', $timeout, \PDO::PARAM_INT);
82+
$stmt->setFetchMode(\PDO::FETCH_COLUMN, 0);
83+
$stmt->execute();
84+
$success = $stmt->fetchColumn();
85+
86+
if ('0' === $success) {
87+
throw new LockConflictedException();
88+
}
89+
90+
// store the release statement in the state
91+
$releaseStmt = $connection->prepare('SELECT RELEASE_LOCK(:key)');
92+
$releaseStmt->bindValue(':key', hash('sha256', $key), \PDO::PARAM_STR);
93+
94+
$key->setState(__CLASS__, $releaseStmt);
95+
}
96+
97+
/**
98+
* {@inheritdoc}
99+
*/
100+
public function putOffExpiration(Key $key, $ttl)
101+
{
102+
// do nothing, the GET_LOCK locks forever, until the session terminates.
103+
}
104+
105+
/**
106+
* {@inheritdoc}
107+
*/
108+
public function delete(Key $key)
109+
{
110+
if (!$key->hasState(__CLASS__)) {
111+
return;
112+
}
113+
114+
$releaseStmt = $key->getState(__CLASS__);
115+
$releaseStmt->execute();
116+
117+
// Close the connection.
118+
$key->removeState(__CLASS__);
119+
}
120+
121+
/**
122+
* {@inheritdoc}
123+
*/
124+
public function exists(Key $key)
125+
{
126+
return $key->hasState(__CLASS__);
127+
}
128+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Lock\Tests\Store;
13+
14+
use Symfony\Component\Lock\Store\MysqlStore;
15+
16+
/**
17+
* @author Jérôme TAMARELLE <jerome@tamarelle.net>
18+
*/
19+
class MysqlStoreTest extends AbstractStoreTest
20+
{
21+
use BlockingStoreTestTrait;
22+
23+
/**
24+
* {@inheritdoc}
25+
*/
26+
public function getStore()
27+
{
28+
return new MysqlStore(array(
29+
'db_dsn' => getenv('MYSQL_DSN'),
30+
'db_username' => getenv('MYSQL_USER'),
31+
'db_password' => getenv('MYSQL_PASSWORD'),
32+
));
33+
}
34+
}

src/Symfony/Component/Lock/phpunit.xml.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
<ini name="error_reporting" value="-1" />
1313
<env name="REDIS_HOST" value="localhost" />
1414
<env name="MEMCACHED_HOST" value="localhost" />
15+
<env name="MYSQL_DSN" value="mysql:host=localhost" />
16+
<env name="MYSQL_USER" value="root" />
17+
<env name="MYSQL_PASSWORD" value="" />
1518
</php>
1619

1720
<testsuites>

0 commit comments

Comments
 (0)