Issue #3428565 by catch, phenaproxima, alexpott: Implement lazy database creation for sessions

(cherry picked from commit da2e2ce2ae)
merge-requests/7287/head
Alex Pott 2024-03-21 08:15:21 +00:00
parent 2071823a8b
commit 9feabd3759
No known key found for this signature in database
GPG Key ID: BDA67E7EE836E5CE
6 changed files with 148 additions and 76 deletions

View File

@ -73,7 +73,7 @@ trait InstallerRedirectTrait {
// Redirect if the database is empty.
if ($connection) {
try {
return !$connection->schema()->tableExists('sessions');
return !$connection->schema()->tableExists('sequences');
}
catch (\Exception $e) {
// If we still have an exception at this point, we need to be careful

View File

@ -5,6 +5,7 @@ namespace Drupal\Core\Session;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\DatabaseException;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
@ -62,10 +63,15 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface {
public function read(#[\SensitiveParameter] string $sid): string|false {
$data = '';
if (!empty($sid)) {
// Read the session data from the database.
$query = $this->connection
->queryRange('SELECT [session] FROM {sessions} WHERE [sid] = :sid', 0, 1, [':sid' => Crypt::hashBase64($sid)]);
$data = (string) $query->fetchField();
try {
// Read the session data from the database.
$query = $this->connection
->queryRange('SELECT [session] FROM {sessions} WHERE [sid] = :sid', 0, 1, [':sid' => Crypt::hashBase64($sid)]);
$data = (string) $query->fetchField();
}
// Swallow the error if the table hasn't been created yet.
catch (\Exception) {
}
}
return $data;
}
@ -74,6 +80,7 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface {
* {@inheritdoc}
*/
public function write(#[\SensitiveParameter] string $sid, string $value): bool {
$try_again = FALSE;
$request = $this->requestStack->getCurrentRequest();
$fields = [
'uid' => $request->getSession()->get('uid', 0),
@ -81,10 +88,27 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface {
'session' => $value,
'timestamp' => $this->time->getRequestTime(),
];
$this->connection->merge('sessions')
->keys(['sid' => Crypt::hashBase64($sid)])
->fields($fields)
->execute();
$doWrite = fn() =>
$this->connection->merge('sessions')
->keys(['sid' => Crypt::hashBase64($sid)])
->fields($fields)
->execute();
try {
$doWrite();
}
catch (\Exception $e) {
// If there was an exception, try to create the table.
if (!$try_again = $this->ensureTableExists()) {
// If the exception happened for other reason than the missing
// table, propagate the exception.
throw $e;
}
}
// Now that the bin has been created, try again if necessary.
if ($try_again) {
$doWrite();
}
return TRUE;
}
@ -99,10 +123,15 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface {
* {@inheritdoc}
*/
public function destroy(#[\SensitiveParameter] string $sid): bool {
// Delete session data.
$this->connection->delete('sessions')
->condition('sid', Crypt::hashBase64($sid))
->execute();
try {
// Delete session data.
$this->connection->delete('sessions')
->condition('sid', Crypt::hashBase64($sid))
->execute();
}
// Swallow the error if the table hasn't been created yet.
catch (\Exception) {
}
return TRUE;
}
@ -116,9 +145,98 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface {
// for three weeks before deleting them, you need to set gc_maxlifetime
// to '1814400'. At that value, only after a user doesn't log in after
// three weeks (1814400 seconds) will their session be removed.
return $this->connection->delete('sessions')
->condition('timestamp', $this->time->getRequestTime() - $lifetime, '<')
->execute();
try {
return $this->connection->delete('sessions')
->condition('timestamp', $this->time->getRequestTime() - $lifetime, '<')
->execute();
}
// Swallow the error if the table hasn't been created yet.
catch (\Exception) {
}
return FALSE;
}
/**
* Defines the schema for the session table.
*
* @internal
*/
protected function schemaDefinition(): array {
$schema = [
'description' => "Drupal's session handlers read and write into the sessions table. Each record represents a user session, either anonymous or authenticated.",
'fields' => [
'uid' => [
'description' => 'The {users}.uid corresponding to a session, or 0 for anonymous user.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
],
'sid' => [
'description' => "A session ID (hashed). The value is generated by Drupal's session handlers.",
'type' => 'varchar_ascii',
'length' => 128,
'not null' => TRUE,
],
'hostname' => [
'description' => 'The IP address that last used this session ID (sid).',
'type' => 'varchar_ascii',
'length' => 128,
'not null' => TRUE,
'default' => '',
],
'timestamp' => [
'description' => 'The Unix timestamp when this session last requested a page. Old records are purged by PHP automatically.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'big',
],
'session' => [
'description' => 'The serialized contents of the user\'s session, an array of name/value pairs that persists across page requests by this session ID. Drupal loads the user\'s session from here at the start of each request and saves it at the end.',
'type' => 'blob',
'not null' => FALSE,
'size' => 'big',
],
],
'primary key' => [
'sid',
],
'indexes' => [
'timestamp' => ['timestamp'],
'uid' => ['uid'],
],
'foreign keys' => [
'session_user' => [
'table' => 'users',
'columns' => ['uid' => 'uid'],
],
],
];
return $schema;
}
/**
* Check if the session table exists and create it if not.
*
* @return bool
* TRUE if the table already exists or was created, FALSE if creation fails.
*/
protected function ensureTableExists(): bool {
try {
$database_schema = $this->connection->schema();
$schema_definition = $this->schemaDefinition();
$database_schema->createTable('sessions', $schema_definition);
}
// If another process has already created the table, attempting to create
// it will throw an exception. In this case just catch the exception and do
// nothing.
catch (DatabaseException $e) {
}
catch (\Exception $e) {
return FALSE;
}
return TRUE;
}
}

View File

@ -234,9 +234,14 @@ class SessionManager extends NativeSessionStorage implements SessionManagerInter
if (!$this->writeSafeHandler->isSessionWritable() || $this->isCli()) {
return;
}
$this->connection->delete('sessions')
->condition('uid', $uid)
->execute();
// The sessions table may not have been created yet.
try {
$this->connection->delete('sessions')
->condition('uid', $uid)
->execute();
}
catch (\Exception) {
}
}
/**

View File

@ -26,6 +26,8 @@ class DbDumpTest extends DriverSpecificKernelTestBase {
* {@inheritdoc}
*/
protected static $modules = [
// @todo system can be removed from this test once
// https://www.drupal.org/project/drupal/issues/2851705 is committed.
'system',
'config',
'dblog',
@ -87,7 +89,6 @@ class DbDumpTest extends DriverSpecificKernelTestBase {
parent::setUp();
// Create some schemas so our export contains tables.
$this->installSchema('system', ['sessions']);
$this->installSchema('dblog', ['watchdog']);
$this->installEntitySchema('block_content');
$this->installEntitySchema('user');
@ -131,7 +132,6 @@ class DbDumpTest extends DriverSpecificKernelTestBase {
'menu_link_content_data',
'menu_link_content_revision',
'menu_link_content_field_revision',
'sessions',
'path_alias',
'path_alias_revision',
'user__roles',

View File

@ -1621,57 +1621,6 @@ function system_schema() {
'primary key' => ['value'],
];
$schema['sessions'] = [
'description' => "Drupal's session handlers read and write into the sessions table. Each record represents a user session, either anonymous or authenticated.",
'fields' => [
'uid' => [
'description' => 'The {users}.uid corresponding to a session, or 0 for anonymous user.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
],
'sid' => [
'description' => "A session ID (hashed). The value is generated by Drupal's session handlers.",
'type' => 'varchar_ascii',
'length' => 128,
'not null' => TRUE,
],
'hostname' => [
'description' => 'The IP address that last used this session ID (sid).',
'type' => 'varchar_ascii',
'length' => 128,
'not null' => TRUE,
'default' => '',
],
'timestamp' => [
'description' => 'The Unix timestamp when this session last requested a page. Old records are purged by PHP automatically.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'big',
],
'session' => [
'description' => 'The serialized contents of the user\'s session, an array of name/value pairs that persists across page requests by this session ID. Drupal loads the user\'s session from here at the start of each request and saves it at the end.',
'type' => 'blob',
'not null' => FALSE,
'size' => 'big',
],
],
'primary key' => [
'sid',
],
'indexes' => [
'timestamp' => ['timestamp'],
'uid' => ['uid'],
],
'foreign keys' => [
'session_user' => [
'table' => 'users',
'columns' => ['uid' => 'uid'],
],
],
];
return $schema;
}

View File

@ -27,7 +27,7 @@ class InstallerRedirectTraitTest extends KernelTestBase {
* - Exceptions to be handled by shouldRedirectToInstaller()
* - Whether or not there is a database connection.
* - Whether or not there is database connection info.
* - Whether or not there exists a sessions table in the database.
* - Whether or not there exists a sequences table in the database.
*/
public static function providerShouldRedirectToInstaller() {
return [
@ -67,7 +67,7 @@ class InstallerRedirectTraitTest extends KernelTestBase {
* @covers ::shouldRedirectToInstaller
* @dataProvider providerShouldRedirectToInstaller
*/
public function testShouldRedirectToInstaller($expected, $exception, $connection, $connection_info, $session_table_exists = TRUE) {
public function testShouldRedirectToInstaller($expected, $exception, $connection, $connection_info, $sequences_table_exists = TRUE) {
try {
throw new $exception();
}
@ -106,8 +106,8 @@ class InstallerRedirectTraitTest extends KernelTestBase {
$schema->expects($this->any())
->method('tableExists')
->with('sessions')
->willReturn($session_table_exists);
->with('sequences')
->willReturn($sequences_table_exists);
$connection->expects($this->any())
->method('schema')