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 d341105

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

File tree

6 files changed

+511
-2
lines changed
Filter options

6 files changed

+511
-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

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

0 commit comments

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