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 9474de0

Browse filesBrowse files
committed
Add a PdoStore in lock
1 parent c81f88f commit 9474de0
Copy full SHA for 9474de0

File tree

6 files changed

+483
-2
lines changed
Filter options

6 files changed

+483
-2
lines changed

‎src/Symfony/Component/Lock/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Lock/CHANGELOG.md
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
4.2.0
5+
-----
6+
7+
* added the Pdo Store
8+
49
3.4.0
510
-----
611

+350Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
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 Doctrine\DBAL\Connection;
15+
use Doctrine\DBAL\DBALException;
16+
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
17+
use Doctrine\DBAL\Schema\Schema;
18+
use Symfony\Component\Lock\Exception\InvalidArgumentException;
19+
use Symfony\Component\Lock\Exception\LockConflictedException;
20+
use Symfony\Component\Lock\Exception\LockExpiredException;
21+
use Symfony\Component\Lock\Exception\NotSupportedException;
22+
use Symfony\Component\Lock\Key;
23+
use Symfony\Component\Lock\StoreInterface;
24+
25+
/**
26+
* PdoStore is a StoreInterface implementation using a PDO connection.
27+
*
28+
* Lock metadata are stored in a table. You can use createTable() to initialize
29+
* a correctly defined table.
30+
31+
* CAUTION: This store relies on all client and server nodes to have
32+
* synchronized clocks for lock expiry to occur at the correct time.
33+
* To ensure locks don't expire prematurely; the ttl's should be set with enough
34+
* extra time to account for any clock drift between nodes.
35+
*
36+
* @author Jérémy Derussé <jeremy@derusse.com>
37+
*/
38+
class PdoStore implements StoreInterface
39+
{
40+
private $conn;
41+
private $dsn;
42+
private $driver;
43+
private $table = 'lock_keys';
44+
private $idCol = 'key_id';
45+
private $tokenCol = 'key_token';
46+
private $expirationCol = 'key_expiration';
47+
private $username = '';
48+
private $password = '';
49+
private $connectionOptions = array();
50+
51+
private $gcProbability;
52+
private $initialTtl;
53+
54+
/**
55+
* You can either pass an existing database connection as PDO instance or
56+
* a Doctrine DBAL Connection or a DSN string that will be used to
57+
* lazy-connect to the database when the lock is actually used.
58+
*
59+
* List of available options:
60+
* * db_table: The name of the table [default: lock_keys]
61+
* * db_id_col: The column where to store the lock key [default: key_id]
62+
* * db_token_col: The column where to store the lock token [default: key_token]
63+
* * db_expiration_col: The column where to store the expiration [default: key_expiration]
64+
* * db_username: The username when lazy-connect [default: '']
65+
* * db_password: The password when lazy-connect [default: '']
66+
* * db_connection_options: An array of driver-specific connection options [default: array()]
67+
*
68+
* @param \PDO|Connection|string $connOrDsn A \PDO or Connection instance or DSN string or null
69+
* @param array $options An associative array of options
70+
* @param float $gcProbability Probability expressed as floating number between 0 and 1 to clean old locks
71+
* @param int $initialTtl The expiration delay of locks in seconds
72+
*
73+
* @throws InvalidArgumentException When first argument is not PDO nor Connection nor string
74+
* @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
75+
* @throws InvalidArgumentException When namespace contains invalid characters
76+
*/
77+
public function __construct($connOrDsn, array $options = array(), float $gcProbability = 0.01, int $initialTtl = 300)
78+
{
79+
if ($gcProbability < 0 || $gcProbability > 1) {
80+
throw new InvalidArgumentException(sprintf('"%s" requires gcProbability between 0 and 1, "%f" given.', __CLASS__, $gcProbability));
81+
}
82+
if ($initialTtl < 1) {
83+
throw new InvalidArgumentException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
84+
}
85+
86+
if ($connOrDsn instanceof \PDO) {
87+
if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
88+
throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __CLASS__));
89+
}
90+
91+
$this->conn = $connOrDsn;
92+
} elseif ($connOrDsn instanceof Connection) {
93+
$this->conn = $connOrDsn;
94+
} elseif (is_string($connOrDsn)) {
95+
$this->dsn = $connOrDsn;
96+
} else {
97+
throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance or DSN string as first argument, "%s" given.', __CLASS__, is_object($connOrDsn) ? get_class($connOrDsn) : gettype($connOrDsn)));
98+
}
99+
100+
$this->table = $options['db_table'] ?? $this->table;
101+
$this->idCol = $options['db_id_col'] ?? $this->idCol;
102+
$this->tokenCol = $options['db_token_col'] ?? $this->tokenCol;
103+
$this->expirationCol = $options['db_expiration_col'] ?? $this->expirationCol;
104+
$this->username = $options['db_username'] ?? $this->username;
105+
$this->password = $options['db_password'] ?? $this->password;
106+
$this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions;
107+
108+
$this->gcProbability = $gcProbability;
109+
$this->initialTtl = $initialTtl;
110+
}
111+
112+
/**
113+
* {@inheritdoc}
114+
*/
115+
public function save(Key $key)
116+
{
117+
$key->reduceLifetime($this->initialTtl);
118+
119+
$sql = "INSERT INTO $this->table ($this->idCol, $this->tokenCol, $this->expirationCol) VALUES (:id, :token, :expiration)";
120+
$stmt = $this->getConnection()->prepare($sql);
121+
122+
$stmt->bindValue(':id', $this->getHashedKey($key));
123+
$stmt->bindValue(':token', $this->getToken($key));
124+
$stmt->bindValue(':expiration', time() + $this->initialTtl, \PDO::PARAM_INT);
125+
126+
try {
127+
$stmt->execute();
128+
if ($key->isExpired()) {
129+
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
130+
}
131+
132+
return;
133+
} catch (UniqueConstraintViolationException $e) {
134+
// the lock is already acquired. It could be us. Let's try to put off.
135+
$this->putOffExpiration($key, $this->initialTtl);
136+
} catch (\PDOException $e) {
137+
// the lock is already acquired. It could be us. Let's try to put off.
138+
$this->putOffExpiration($key, $this->initialTtl);
139+
}
140+
141+
if ($key->isExpired()) {
142+
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
143+
}
144+
145+
if ($this->gcProbability > 0 && (1.0 === $this->gcProbability || (random_int(0, PHP_INT_MAX) / PHP_INT_MAX) <= $this->gcProbability)) {
146+
$this->prune();
147+
}
148+
}
149+
150+
/**
151+
* {@inheritdoc}
152+
*/
153+
public function waitAndSave(Key $key)
154+
{
155+
throw new NotSupportedException(sprintf('The store "%s" does not supports blocking locks.', __CLASS__));
156+
}
157+
158+
/**
159+
* {@inheritdoc}
160+
*/
161+
public function putOffExpiration(Key $key, $ttl)
162+
{
163+
if ($ttl < 1) {
164+
throw new InvalidArgumentException(sprintf('%s() expects a TTL greater or equals to 1. Got %s.', __METHOD__, $ttl));
165+
}
166+
167+
$key->reduceLifetime($ttl);
168+
169+
$sql = "UPDATE $this->table SET $this->expirationCol = :expiration, $this->tokenCol = :token WHERE $this->idCol = :id AND ($this->tokenCol = :token OR $this->expirationCol <= :now)";
170+
$stmt = $this->getConnection()->prepare($sql);
171+
172+
$stmt->bindValue(':id', $this->getHashedKey($key));
173+
$stmt->bindValue(':token', $this->getToken($key));
174+
$stmt->bindValue(':expiration', time() + $ttl, \PDO::PARAM_INT);
175+
$stmt->bindValue(':now', time(), \PDO::PARAM_INT);
176+
$stmt->execute();
177+
178+
// If this method is called twice in the same second, the row wouldnt' be updated. We have to call exists to know if the we are the owner
179+
if (!$stmt->rowCount() && !$this->exists($key)) {
180+
throw new LockConflictedException();
181+
}
182+
183+
if ($key->isExpired()) {
184+
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
185+
}
186+
}
187+
188+
/**
189+
* {@inheritdoc}
190+
*/
191+
public function delete(Key $key)
192+
{
193+
$sql = "DELETE FROM $this->table WHERE $this->idCol = :id AND $this->tokenCol = :token";
194+
$stmt = $this->getConnection()->prepare($sql);
195+
196+
$stmt->bindValue(':id', $this->getHashedKey($key));
197+
$stmt->bindValue(':token', $this->getToken($key));
198+
$stmt->execute();
199+
}
200+
201+
/**
202+
* {@inheritdoc}
203+
*/
204+
public function exists(Key $key)
205+
{
206+
$sql = "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND $this->tokenCol = :token AND $this->expirationCol > :expiration";
207+
$stmt = $this->getConnection()->prepare($sql);
208+
209+
$stmt->bindValue(':id', $this->getHashedKey($key));
210+
$stmt->bindValue(':token', $this->getToken($key));
211+
$stmt->bindValue(':expiration', time(), \PDO::PARAM_INT);
212+
$stmt->execute();
213+
214+
return (bool) $stmt->fetchColumn();
215+
}
216+
217+
/**
218+
* Returns an hashed version of the key.
219+
*/
220+
private function getHashedKey(Key $key): string
221+
{
222+
return hash('sha256', $key);
223+
}
224+
225+
/**
226+
* Retrieve an unique token for the given key.
227+
*/
228+
private function getToken(Key $key): string
229+
{
230+
if (!$key->hasState(__CLASS__)) {
231+
$token = base64_encode(random_bytes(32));
232+
$key->setState(__CLASS__, $token);
233+
}
234+
235+
return $key->getState(__CLASS__);
236+
}
237+
238+
/**
239+
* @return \PDO|Connection
240+
*/
241+
private function getConnection()
242+
{
243+
if (null === $this->conn) {
244+
$this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions);
245+
$this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
246+
}
247+
if (null === $this->driver) {
248+
if ($this->conn instanceof \PDO) {
249+
$this->driver = $this->conn->getAttribute(\PDO::ATTR_DRIVER_NAME);
250+
} else {
251+
switch ($this->driver = $this->conn->getDriver()->getName()) {
252+
case 'mysqli':
253+
case 'pdo_mysql':
254+
case 'drizzle_pdo_mysql':
255+
$this->driver = 'mysql';
256+
break;
257+
case 'pdo_sqlite':
258+
$this->driver = 'sqlite';
259+
break;
260+
case 'pdo_pgsql':
261+
$this->driver = 'pgsql';
262+
break;
263+
case 'oci8':
264+
case 'pdo_oracle':
265+
$this->driver = 'oci';
266+
break;
267+
case 'pdo_sqlsrv':
268+
$this->driver = 'sqlsrv';
269+
break;
270+
}
271+
}
272+
}
273+
274+
return $this->conn;
275+
}
276+
277+
/**
278+
* Creates the table to store lock keys which can be called once for setup.
279+
*
280+
* @throws \PDOException When the table already exists
281+
* @throws DBALException When the table already exists
282+
* @throws \DomainException When an unsupported PDO driver is used
283+
*/
284+
public function createTable()
285+
{
286+
// connect if we are not yet
287+
$conn = $this->getConnection();
288+
289+
if ($conn instanceof Connection) {
290+
$types = array(
291+
'mysql' => 'binary',
292+
'sqlite' => 'text',
293+
'pgsql' => 'string',
294+
'oci' => 'string',
295+
'sqlsrv' => 'string',
296+
);
297+
if (!isset($types[$this->driver])) {
298+
throw new \DomainException(sprintf('Creating the lock table is currently not implemented for PDO driver "%s".', $this->driver));
299+
}
300+
301+
$schema = new Schema();
302+
$table = $schema->createTable($this->table);
303+
$table->addColumn($this->idCol, 'string', array('length' => 64));
304+
$table->addColumn($this->tokenCol, 'string', array('length' => 44));
305+
$table->addColumn($this->expirationCol, 'integer', array('unsigned' => true));
306+
$table->setPrimaryKey(array($this->idCol));
307+
308+
foreach ($schema->toSql($conn->getDatabasePlatform()) as $sql) {
309+
$conn->exec($sql);
310+
}
311+
312+
return;
313+
}
314+
315+
switch ($this->driver) {
316+
case 'mysql':
317+
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(44) NOT NULL, $this->expirationCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB";
318+
break;
319+
case 'sqlite':
320+
$sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->tokenCol TEXT NOT NULL, $this->expirationCol INTEGER)";
321+
break;
322+
case 'pgsql':
323+
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(64) NOT NULL, $this->expirationCol INTEGER)";
324+
break;
325+
case 'oci':
326+
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR2(64) NOT NULL, $this->expirationCol INTEGER)";
327+
break;
328+
case 'sqlsrv':
329+
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(64) NOT NULL, $this->expirationCol INTEGER)";
330+
break;
331+
default:
332+
throw new \DomainException(sprintf('Creating the lock table is currently not implemented for PDO driver "%s".', $this->driver));
333+
}
334+
335+
$conn->exec($sql);
336+
}
337+
338+
/**
339+
* {@inheritdoc}
340+
*/
341+
private function prune()
342+
{
343+
$sql = "DELETE FROM $this->table WHERE $this->expirationCol <= :expiration";
344+
345+
$stmt = $this->getConnection()->prepare($sql);
346+
$stmt->bindValue(':expiration', time(), \PDO::PARAM_INT);
347+
348+
$stmt->execute();
349+
}
350+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php
+5-1Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ trait BlockingStoreTestTrait
2222
{
2323
/**
2424
* @see AbstractStoreTest::getStore()
25+
*
26+
* @return StoreInterface
2527
*/
2628
abstract protected function getStore();
2729

@@ -40,7 +42,6 @@ public function testBlockingLocks()
4042
$clockDelay = 50000;
4143

4244
/** @var StoreInterface $store */
43-
$store = $this->getStore();
4445
$key = new Key(uniqid(__METHOD__, true));
4546
$parentPID = posix_getpid();
4647

@@ -51,6 +52,7 @@ public function testBlockingLocks()
5152
// Wait the start of the child
5253
pcntl_sigwaitinfo(array(SIGHUP), $info);
5354

55+
$store = $this->getStore();
5456
try {
5557
// This call should failed given the lock should already by acquired by the child
5658
$store->save($key);
@@ -72,6 +74,8 @@ public function testBlockingLocks()
7274
} else {
7375
// Block SIGHUP signal
7476
pcntl_sigprocmask(SIG_BLOCK, array(SIGHUP));
77+
78+
$store = $this->getStore();
7579
try {
7680
$store->save($key);
7781
// send the ready signal to the parent

0 commit comments

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