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 7611a62

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

File tree

4 files changed

+470
-1
lines changed
Filter options

4 files changed

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

0 commit comments

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