Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 828ac17

Browse filesBrowse files
committed
[Lock] Inject and reuse a PDO or DBAL connection in MysqlStore
1 parent 74bf72e commit 828ac17
Copy full SHA for 828ac17

File tree

Expand file treeCollapse file tree

3 files changed

+104
-51
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+104
-51
lines changed

‎src/Symfony/Component/Lock/Store/MysqlStore.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Lock/Store/MysqlStore.php
+39-26Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
namespace Symfony\Component\Lock\Store;
1313

14+
use Doctrine\DBAL\Connection;
15+
use Symfony\Component\Lock\Exception\InvalidArgumentException;
16+
use Symfony\Component\Lock\Exception\LockAcquiringException;
1417
use Symfony\Component\Lock\Exception\LockConflictedException;
1518
use Symfony\Component\Lock\Key;
1619
use Symfony\Component\Lock\StoreInterface;
@@ -22,29 +25,32 @@
2225
*/
2326
class MysqlStore implements StoreInterface
2427
{
25-
private $dsn;
26-
private $username;
27-
private $password;
28-
private $options;
28+
/**
29+
* @var \PDO|Connection
30+
*/
31+
private $connection;
2932
private $waitTimeout;
3033

3134
/**
32-
* List of available options:
33-
* * db_username: The username when lazy-connect [default: '']
34-
* * db_password: The password when lazy-connect [default: '']
35-
* * db_connection_options: An array of driver-specific connection options [default: array()]
36-
* * wait_timeout: Time in seconds to wait for a lock to be released. A negative value means infinite. [default: -1].
37-
*
38-
* @param string $dsn The connection DSN string
39-
* @param array $options configuration options
35+
* @param \PDO|Connection $connection
36+
* @param int $waitTimeout Time in seconds to wait for a lock to be released. A negative value means infinite.
4037
*/
41-
public function __construct($dsn, array $options)
38+
public function __construct($connection, $waitTimeout = -1)
4239
{
43-
$this->dsn = $dsn;
44-
$this->username = $options['db_username'] ?? '';
45-
$this->password = $options['db_password'] ?? '';
46-
$this->options = $options['db_connection_options'] ?? array();
47-
$this->waitTimeout = $options['wait_timeout'] ?? -1;
40+
if ($connection instanceof \PDO) {
41+
if ('mysql' !== $driver = $connection->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
42+
throw new InvalidArgumentException(sprintf('%s requires a "mysql" connection. "%s" given.', __CLASS__, $driver));
43+
}
44+
} elseif ($connection instanceof Connection) {
45+
if ('pdo_mysql' !== $driver = $connection->getDriver()->getName()) {
46+
throw new InvalidArgumentException(sprintf('%s requires a "pdo_mysql" connection. "%s" given.', __CLASS__, $driver));
47+
}
48+
} else {
49+
throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance, "%s" given.', __CLASS__, is_object($connection) ? get_class($connection) : gettype($connection)));
50+
}
51+
52+
$this->connection = $connection;
53+
$this->waitTimeout = $waitTimeout;
4854
}
4955

5056
/**
@@ -73,23 +79,31 @@ private function lock(Key $key, bool $blocking)
7379
// no timeout for impatient
7480
$timeout = $blocking ? $this->waitTimeout : 0;
7581

76-
$connection = new \PDO($this->dsn, $this->username, $this->password, $this->options);
77-
$connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
82+
// Hash the key to guarantee it contains between 1 and 64 characters
83+
$storedKey = hash('sha256', $key);
7884

79-
$stmt = $connection->prepare('SELECT GET_LOCK(:key, :timeout)');
80-
$stmt->bindValue(':key', hash('sha256', $key), \PDO::PARAM_STR);
85+
$stmt = $this->connection->prepare('SELECT IF(IS_USED_LOCK(:key) = CONNECTION_ID(), -1, GET_LOCK(:key, :timeout))');
86+
$stmt->bindValue(':key', $storedKey, \PDO::PARAM_STR);
8187
$stmt->bindValue(':timeout', $timeout, \PDO::PARAM_INT);
8288
$stmt->setFetchMode(\PDO::FETCH_COLUMN, 0);
8389
$stmt->execute();
90+
91+
// 1: Lock successful
92+
// 0: Already locked by another session
93+
// -1: Already locked by the same session
8494
$success = $stmt->fetchColumn();
8595

86-
if ('0' === $success) {
96+
if ($blocking && '-1' === $success) {
97+
throw new LockAcquiringException('Lock already acquired with the same MySQL connection.');
98+
}
99+
100+
if ('1' !== $success) {
87101
throw new LockConflictedException();
88102
}
89103

90104
// 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);
105+
$releaseStmt = $this->connection->prepare('SELECT RELEASE_LOCK(:key)');
106+
$releaseStmt->bindValue(':key', $storedKey, \PDO::PARAM_STR);
93107

94108
$key->setState(__CLASS__, $releaseStmt);
95109
}
@@ -114,7 +128,6 @@ public function delete(Key $key)
114128
$releaseStmt = $key->getState(__CLASS__);
115129
$releaseStmt->execute();
116130

117-
// Close the connection.
118131
$key->removeState(__CLASS__);
119132
}
120133

‎src/Symfony/Component/Lock/Tests/Store/MysqlStoreTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Lock/Tests/Store/MysqlStoreTest.php
+64-25Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace Symfony\Component\Lock\Tests\Store;
1313

14-
use Symfony\Component\Lock\Exception\LockConflictedException;
14+
use Doctrine\DBAL\DriverManager;
1515
use Symfony\Component\Lock\Key;
1616
use Symfony\Component\Lock\Store\MysqlStore;
1717

@@ -22,45 +22,84 @@ class MysqlStoreTest extends AbstractStoreTest
2222
{
2323
use BlockingStoreTestTrait;
2424

25+
private $connectionCase = 'pdo';
26+
2527
/**
2628
* {@inheritdoc}
2729
*/
2830
public function getStore()
2931
{
30-
return new MysqlStore('mysql:host='.getenv('MYSQL_HOST'), array(
31-
'db_username' => getenv('MYSQL_USERNAME'),
32-
'db_password' => getenv('MYSQL_PASSWORD'),
33-
'wait_timeout' => 1,
34-
));
32+
switch ($this->connectionCase) {
33+
case 'pdo':
34+
$connection = new \PDO('mysql:host='.getenv('MYSQL_HOST'), getenv('MYSQL_USERNAME'), getenv('MYSQL_PASSWORD'));
35+
$connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
36+
break;
37+
38+
case 'dbal':
39+
$connection = DriverManager::getConnection(array(
40+
'driver' => 'pdo_mysql',
41+
'user' => getenv('MYSQL_USERNAME'),
42+
'password' => getenv('MYSQL_PASSWORD'),
43+
'host' => getenv('MYSQL_HOST'),
44+
));
45+
break;
46+
}
47+
48+
return new MysqlStore($connection);
3549
}
3650

37-
public function testConfigurableWaitTimeout()
51+
public function testSaveWithDoctrineDBAL()
3852
{
39-
$store = new MysqlStore('mysql:host='.getenv('MYSQL_HOST'), array(
40-
'db_username' => getenv('MYSQL_USERNAME'),
41-
'db_password' => getenv('MYSQL_PASSWORD'),
42-
'wait_timeout' => 1,
43-
));
53+
if (!class_exists(DriverManager::class)) {
54+
$this->markTestSkipped('Package doctrine/dbal is required.');
55+
}
4456

45-
$resource = uniqid(__METHOD__, true);
46-
$key1 = new Key($resource);
47-
$key2 = new Key($resource);
57+
$this->connectionCase = 'dbal';
4858

49-
$store->save($key1);
59+
parent::testSave();
60+
}
5061

51-
$startTime = microtime(true);
62+
/**
63+
* @expectedException \InvalidArgumentException
64+
* @expectedExceptionMessage Symfony\Component\Lock\Store\MysqlStore requires a "mysql" connection. "sqlite" given.
65+
*/
66+
public function testOnlyMySQLDatabaseIsSupported()
67+
{
68+
$connection = new \PDO('sqlite::memory:');
5269

53-
try {
54-
$store->waitAndSave($key2);
70+
return new MysqlStore($connection);
71+
}
5572

56-
$this->fail('The store shouldn\'t save the second key');
57-
} catch (LockConflictedException $e) {
58-
// Expected
73+
/**
74+
* @expectedException \InvalidArgumentException
75+
* @expectedExceptionMessage Symfony\Component\Lock\Store\MysqlStore requires a "pdo_mysql" connection. "pdo_sqlite" given.
76+
*/
77+
public function testOnlyMySQLDatabaseIsSupportedWithDoctrineDBAL()
78+
{
79+
if (!class_exists(DriverManager::class)) {
80+
$this->markTestSkipped('Package doctrine/dbal is required.');
5981
}
6082

61-
$waitTime = microtime(true) - $startTime;
83+
$connection = DriverManager::getConnection(array(
84+
'driver' => 'pdo_sqlite',
85+
));
86+
87+
return new MysqlStore($connection);
88+
}
6289

63-
$this->assertGreaterThanOrEqual(1, $waitTime);
64-
$this->assertLessThan(2, $waitTime);
90+
/**
91+
* @expectedException \Symfony\Component\Lock\Exception\LockAcquiringException
92+
* @expectedExceptionMessage Lock already acquired with the same MySQL connection.
93+
*/
94+
public function testWaitTheSameResourceOnTheSameConnectionIsNotSupported()
95+
{
96+
$store = $this->getStore();
97+
98+
$resource = uniqid(__METHOD__, true);
99+
$key1 = new Key($resource);
100+
$key2 = new Key($resource);
101+
102+
$store->save($key1);
103+
$store->waitAndSave($key2);
65104
}
66105
}

‎src/Symfony/Component/Lock/composer.json

Copy file name to clipboardExpand all lines: src/Symfony/Component/Lock/composer.json
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"psr/log": "~1.0"
2121
},
2222
"require-dev": {
23+
"doctrine/dbal": "~2.4",
2324
"predis/predis": "~1.0"
2425
},
2526
"autoload": {

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.