Skip to content

Commit e849cbb

Browse files
azjezzbcremerrobfrawley
committed
[HttpFoundation] Precalculate session expiry timestamp
Co-authored-by: Benjamin Cremer <b.cremer@shopware.com> Co-authored-by: Rob Frawley 2nd <rmf@src.run>
1 parent 320e495 commit e849cbb

File tree

3 files changed

+45
-23
lines changed

3 files changed

+45
-23
lines changed

UPGRADE-4.4.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ HttpFoundation
109109

110110
* `ApacheRequest` is deprecated, use `Request` class instead.
111111
* Passing a third argument to `HeaderBag::get()` is deprecated since Symfony 4.4, use method `all()` instead
112+
* `PdoSessionHandler` now precalculates the expiry timestamp in the lifetime column,
113+
make sure to run `CREATE INDEX EXPIRY ON sessions (sess_lifetime)` to update your database
114+
to speed up garbage collection of expired sessions.
112115

113116
HttpKernel
114117
----------

src/Symfony/Component/HttpFoundation/CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ CHANGELOG
77
* passing arguments to `Request::isMethodSafe()` is deprecated.
88
* `ApacheRequest` is deprecated, use the `Request` class instead.
99
* passing a third argument to `HeaderBag::get()` is deprecated, use method `all()` instead
10-
10+
* `PdoSessionHandler` now precalculates the expiry timestamp in the lifetime column,
11+
make sure to run `CREATE INDEX EXPIRY ON sessions (sess_lifetime)` to update your database
12+
to speed up garbage collection of expired sessions.
13+
1114
4.3.0
1215
-----
1316

src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ class PdoSessionHandler extends AbstractSessionHandler
6565
*/
6666
const LOCK_TRANSACTIONAL = 2;
6767

68+
private const MAX_LIFETIME = 315576000;
69+
6870
/**
6971
* @var \PDO|null PDO instance or null when not connected yet
7072
*/
@@ -237,6 +239,7 @@ public function createTable()
237239

238240
try {
239241
$this->pdo->exec($sql);
242+
$this->pdo->exec("CREATE INDEX EXPIRY ON $this->table ($this->lifetimeCol)");
240243
} catch (\PDOException $e) {
241244
$this->rollback();
242245

@@ -368,14 +371,14 @@ protected function doWrite($sessionId, $data)
368371
*/
369372
public function updateTimestamp($sessionId, $data)
370373
{
371-
$maxlifetime = (int) ini_get('session.gc_maxlifetime');
374+
$expiry = time() + (int) ini_get('session.gc_maxlifetime');
372375

373376
try {
374377
$updateStmt = $this->pdo->prepare(
375-
"UPDATE $this->table SET $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id"
378+
"UPDATE $this->table SET $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id"
376379
);
377380
$updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
378-
$updateStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
381+
$updateStmt->bindParam(':expiry', $expiry, \PDO::PARAM_INT);
379382
$updateStmt->bindValue(':time', time(), \PDO::PARAM_INT);
380383
$updateStmt->execute();
381384
} catch (\PDOException $e) {
@@ -402,14 +405,21 @@ public function close()
402405
$this->gcCalled = false;
403406

404407
// delete the session records that have expired
408+
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol < :time AND $this->lifetimeCol > :min";
409+
$stmt = $this->pdo->prepare($sql);
410+
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
411+
$stmt->bindValue(':min', self::MAX_LIFETIME, \PDO::PARAM_INT);
412+
$stmt->execute();
413+
// to be removed in 6.0
405414
if ('mysql' === $this->driver) {
406-
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol < :time";
415+
$legacySql = "DELETE FROM $this->table WHERE $this->lifetimeCol <= :min AND $this->lifetimeCol + $this->timeCol < :time";
407416
} else {
408-
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol < :time - $this->timeCol";
417+
$legacySql = "DELETE FROM $this->table WHERE $this->lifetimeCol <= :min AND $this->lifetimeCol < :time - $this->timeCol";
409418
}
410419

411-
$stmt = $this->pdo->prepare($sql);
420+
$stmt = $this->pdo->prepare($legacySql);
412421
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
422+
$stmt->bindValue(':min', self::MAX_LIFETIME, \PDO::PARAM_INT);
413423
$stmt->execute();
414424
}
415425

@@ -616,7 +626,12 @@ protected function doRead($sessionId)
616626
$sessionRows = $selectStmt->fetchAll(\PDO::FETCH_NUM);
617627

618628
if ($sessionRows) {
619-
if ($sessionRows[0][1] + $sessionRows[0][2] < time()) {
629+
$expiry = (int) $sessionRows[0][1];
630+
if ($expiry <= self::MAX_LIFETIME) {
631+
$expiry += $sessionRows[0][2];
632+
}
633+
634+
if ($expiry < time()) {
620635
$this->sessionExpired = true;
621636

622637
return '';
@@ -747,6 +762,7 @@ private function getSelectSql(): string
747762
if (self::LOCK_TRANSACTIONAL === $this->lockMode) {
748763
$this->beginTransaction();
749764

765+
// In the future, we should avoid selecting {$this->>timeCol} column
750766
switch ($this->driver) {
751767
case 'mysql':
752768
case 'oci':
@@ -775,18 +791,18 @@ private function getInsertStatement(string $sessionId, string $sessionData, int
775791
$data = fopen('php://memory', 'r+');
776792
fwrite($data, $sessionData);
777793
rewind($data);
778-
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, EMPTY_BLOB(), :lifetime, :time) RETURNING $this->dataCol into :data";
794+
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, EMPTY_BLOB(), :expiry, :time) RETURNING $this->dataCol into :data";
779795
break;
780796
default:
781797
$data = $sessionData;
782-
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";
798+
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)";
783799
break;
784800
}
785801

786802
$stmt = $this->pdo->prepare($sql);
787803
$stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
788804
$stmt->bindParam(':data', $data, \PDO::PARAM_LOB);
789-
$stmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
805+
$stmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT);
790806
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
791807

792808
return $stmt;
@@ -802,18 +818,18 @@ private function getUpdateStatement(string $sessionId, string $sessionData, int
802818
$data = fopen('php://memory', 'r+');
803819
fwrite($data, $sessionData);
804820
rewind($data);
805-
$sql = "UPDATE $this->table SET $this->dataCol = EMPTY_BLOB(), $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id RETURNING $this->dataCol into :data";
821+
$sql = "UPDATE $this->table SET $this->dataCol = EMPTY_BLOB(), $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id RETURNING $this->dataCol into :data";
806822
break;
807823
default:
808824
$data = $sessionData;
809-
$sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id";
825+
$sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id";
810826
break;
811827
}
812828

813829
$stmt = $this->pdo->prepare($sql);
814830
$stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
815831
$stmt->bindParam(':data', $data, \PDO::PARAM_LOB);
816-
$stmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
832+
$stmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT);
817833
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
818834

819835
return $stmt;
@@ -826,7 +842,7 @@ private function getMergeStatement(string $sessionId, string $data, int $maxlife
826842
{
827843
switch (true) {
828844
case 'mysql' === $this->driver:
829-
$mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
845+
$mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time) ".
830846
"ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
831847
break;
832848
case 'sqlsrv' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='):
@@ -837,10 +853,10 @@ private function getMergeStatement(string $sessionId, string $data, int $maxlife
837853
"WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
838854
break;
839855
case 'sqlite' === $this->driver:
840-
$mergeSql = "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";
856+
$mergeSql = "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)";
841857
break;
842858
case 'pgsql' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '9.5', '>='):
843-
$mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
859+
$mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time) ".
844860
"ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
845861
break;
846862
default:
@@ -854,15 +870,15 @@ private function getMergeStatement(string $sessionId, string $data, int $maxlife
854870
$mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR);
855871
$mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR);
856872
$mergeStmt->bindParam(3, $data, \PDO::PARAM_LOB);
857-
$mergeStmt->bindParam(4, $maxlifetime, \PDO::PARAM_INT);
858-
$mergeStmt->bindValue(5, time(), \PDO::PARAM_INT);
859-
$mergeStmt->bindParam(6, $data, \PDO::PARAM_LOB);
860-
$mergeStmt->bindParam(7, $maxlifetime, \PDO::PARAM_INT);
861-
$mergeStmt->bindValue(8, time(), \PDO::PARAM_INT);
873+
$mergeStmt->bindValue(4, time() + $maxlifetime, \PDO::PARAM_INT);
874+
$mergeStmt->bindValue(4, time(), \PDO::PARAM_INT);
875+
$mergeStmt->bindParam(5, $data, \PDO::PARAM_LOB);
876+
$mergeStmt->bindValue(6, time() + $maxlifetime, \PDO::PARAM_INT);
877+
$mergeStmt->bindValue(6, time(), \PDO::PARAM_INT);
862878
} else {
863879
$mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
864880
$mergeStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
865-
$mergeStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
881+
$mergeStmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT);
866882
$mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT);
867883
}
868884

0 commit comments

Comments
 (0)