diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php index 32602c7a4f..3e542f5c87 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php @@ -504,54 +504,51 @@ class PdoSessionHandler implements \SessionHandlerInterface $selectSql = $this->getSelectSql(); $selectStmt = $this->pdo->prepare($selectSql); $selectStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $selectStmt->execute(); - $sessionRows = $selectStmt->fetchAll(\PDO::FETCH_NUM); + do { + $selectStmt->execute(); + $sessionRows = $selectStmt->fetchAll(\PDO::FETCH_NUM); - if ($sessionRows) { - if ($sessionRows[0][1] + $sessionRows[0][2] < time()) { - $this->sessionExpired = true; - - return ''; - } - - return is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0]; - } - - if (self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) { - // Exclusive-reading of non-existent rows does not block, so we need to do an insert to block - // until other connections to the session are committed. - try { - $insertStmt = $this->pdo->prepare( - "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)" - ); - $insertStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $insertStmt->bindValue(':data', '', \PDO::PARAM_LOB); - $insertStmt->bindValue(':lifetime', 0, \PDO::PARAM_INT); - $insertStmt->bindValue(':time', time(), \PDO::PARAM_INT); - $insertStmt->execute(); - } catch (\PDOException $e) { - // Catch duplicate key error because other connection created the session already. - // It would only not be the case when the other connection destroyed the session. - if (0 === strpos($e->getCode(), '23')) { - // Retrieve finished session data written by concurrent connection. SELECT - // FOR UPDATE is necessary to avoid deadlock of connection that starts reading - // before we write (transform intention to real lock). - $selectStmt->execute(); - $sessionRows = $selectStmt->fetchAll(\PDO::FETCH_NUM); - - if ($sessionRows) { - return is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0]; - } + if ($sessionRows) { + if ($sessionRows[0][1] + $sessionRows[0][2] < time()) { + $this->sessionExpired = true; return ''; } - throw $e; + return is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0]; } - } - return ''; + if (self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) { + // Exclusive-reading of non-existent rows does not block, so we need to do an insert to block + // until other connections to the session are committed. + try { + $insertStmt = $this->pdo->prepare( + "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)" + ); + $insertStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $insertStmt->bindValue(':data', '', \PDO::PARAM_LOB); + $insertStmt->bindValue(':lifetime', 0, \PDO::PARAM_INT); + $insertStmt->bindValue(':time', time(), \PDO::PARAM_INT); + $insertStmt->execute(); + } catch (\PDOException $e) { + // Catch duplicate key error because other connection created the session already. + // It would only not be the case when the other connection destroyed the session. + if (0 === strpos($e->getCode(), '23')) { + // Retrieve finished session data written by concurrent connection by restarting the loop. + // We have to start a new transaction as a failed query will mark the current transaction as + // aborted in PostgreSQL and disallow further queries within it. + $this->rollback(); + $this->beginTransaction(); + continue; + } + + throw $e; + } + } + + return ''; + } while (true); } /** diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php index 9612320976..1f88d00d8f 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -362,4 +362,8 @@ class MockPdo extends \PDO public function beginTransaction() { } + + public function rollBack() + { + } }