diff --git a/core/core.services.yml b/core/core.services.yml index 5e847793155..43696eae155 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -358,6 +358,11 @@ services: class: Drupal\Core\Database\Connection factory: Drupal\Core\Database\Database::getConnection arguments: [replica] + database.replica_kill_switch: + class: Drupal\Core\Database\ReplicaKillSwitch + arguments: ['@settings', '@datetime.time', '@session'] + tags: + - { name: event_subscriber } datetime.time: class: Drupal\Component\Datetime\Time arguments: ['@request_stack'] @@ -939,7 +944,7 @@ services: - { name: needs_destruction } menu.rebuild_subscriber: class: Drupal\Core\EventSubscriber\MenuRouterRebuildSubscriber - arguments: ['@lock', '@plugin.manager.menu.link', '@database'] + arguments: ['@lock', '@plugin.manager.menu.link', '@database', '@database.replica_kill_switch'] tags: - { name: event_subscriber } path.alias_storage: @@ -1403,10 +1408,6 @@ services: tags: - { name: backend_overridable } lazy: true - replica_database_ignore__subscriber: - class: Drupal\Core\EventSubscriber\ReplicaDatabaseIgnoreSubscriber - tags: - - {name: event_subscriber} country_manager: class: Drupal\Core\Locale\CountryManager arguments: ['@module_handler'] diff --git a/core/includes/database.inc b/core/includes/database.inc index 1b133263eb4..e5927fa65b9 100644 --- a/core/includes/database.inc +++ b/core/includes/database.inc @@ -11,7 +11,6 @@ use Drupal\Core\Database\Database; use Drupal\Core\Database\Query\Condition; -use Drupal\Core\Site\Settings; /** * @addtogroup database @@ -1089,18 +1088,14 @@ function db_change_field($table, $field, $field_new, $spec, $keys_new = []) { * Sets a session variable specifying the lag time for ignoring a replica * server (A replica server is traditionally referred to as * a "slave" in database server documentation). + * + * @deprecated as of Drupal 8.7.x, will be removed in Drupal 9.0.0. Use + * \Drupal::service('database.replica_kill_switch')->trigger() instead. + * + * @see https://www.drupal.org/node/2997500 * @see https://www.drupal.org/node/2275877 */ function db_ignore_replica() { - $connection_info = Database::getConnectionInfo(); - // Only set ignore_replica_server if there are replica servers being used, - // which is assumed if there are more than one. - if (count($connection_info) > 1) { - // Five minutes is long enough to allow the replica to break and resume - // interrupted replication without causing problems on the Drupal site from - // the old data. - $duration = Settings::get('maximum_replication_lag', 300); - // Set session variable with amount of time to delay before using replica. - $_SESSION['ignore_replica_server'] = REQUEST_TIME + $duration; - } + @trigger_error('db_ignore_replica() is deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.0. Use \Drupal\Core\Database\ReplicaKillSwitch::trigger() instead. See https://www.drupal.org/node/2997500', E_USER_DEPRECATED); + \Drupal::service('database.replica_kill_switch')->trigger(); } diff --git a/core/lib/Drupal/Core/Database/ReplicaKillSwitch.php b/core/lib/Drupal/Core/Database/ReplicaKillSwitch.php new file mode 100644 index 00000000000..1140e220de2 --- /dev/null +++ b/core/lib/Drupal/Core/Database/ReplicaKillSwitch.php @@ -0,0 +1,113 @@ +settings = $settings; + $this->time = $time; + $this->session = $session; + } + + /** + * Denies access to replica database on the current request. + * + * @see https://www.drupal.org/node/2286193 + */ + public function trigger() { + $connection_info = Database::getConnectionInfo(); + // Only set ignore_replica_server if there are replica servers being used, + // which is assumed if there are more than one. + if (count($connection_info) > 1) { + // Five minutes is long enough to allow the replica to break and resume + // interrupted replication without causing problems on the Drupal site + // from the old data. + $duration = $this->settings->get('maximum_replication_lag', 300); + // Set session variable with amount of time to delay before using replica. + $this->session->set('ignore_replica_server', $this->time->getRequestTime() + $duration); + } + } + + /** + * Checks and disables the replica database server if appropriate. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function checkReplicaServer(GetResponseEvent $event) { + // Ignore replica database servers for this request. + // + // In Drupal's distributed database structure, new data is written to the + // master and then propagated to the replica servers. This means there is a + // lag between when data is written to the master and when it is available + // on the replica. At these times, we will want to avoid using a replica + // server temporarily. For example, if a user posts a new node then we want + // to disable the replica server for that user temporarily to allow the + // replica server to catch up. + // That way, that user will see their changes immediately while for other + // users we still get the benefits of having a replica server, just with + // slightly stale data. Code that wants to disable the replica server should + // use the 'database.replica_kill_switch' service's trigger() method to set + // 'ignore_replica_server' session flag to the timestamp after which the + // replica can be re-enabled. + if ($this->session->has('ignore_replica_server')) { + if ($this->session->get('ignore_replica_server') >= $this->time->getRequestTime()) { + Database::ignoreTarget('default', 'replica'); + } + else { + $this->session->remove('ignore_replica_server'); + } + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[KernelEvents::REQUEST][] = ['checkReplicaServer']; + return $events; + } + +} diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php index bcbb2e6b9f4..150a18d64b6 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -726,7 +726,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt parent::delete($entities); // Ignore replica server temporarily. - db_ignore_replica(); + \Drupal::service('database.replica_kill_switch')->trigger(); } catch (\Exception $e) { $transaction->rollBack(); @@ -777,7 +777,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt $return = parent::save($entity); // Ignore replica server temporarily. - db_ignore_replica(); + \Drupal::service('database.replica_kill_switch')->trigger(); return $return; } catch (\Exception $e) { diff --git a/core/lib/Drupal/Core/EventSubscriber/MenuRouterRebuildSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MenuRouterRebuildSubscriber.php index bc606b4e9cd..a41645f53b6 100644 --- a/core/lib/Drupal/Core/EventSubscriber/MenuRouterRebuildSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/MenuRouterRebuildSubscriber.php @@ -3,6 +3,7 @@ namespace Drupal\Core\EventSubscriber; use Drupal\Core\Cache\Cache; +use Drupal\Core\Database\ReplicaKillSwitch; use Drupal\Core\Lock\LockBackendInterface; use Drupal\Core\Menu\MenuLinkManagerInterface; use Drupal\Core\Routing\RoutingEvents; @@ -27,6 +28,13 @@ class MenuRouterRebuildSubscriber implements EventSubscriberInterface { */ protected $menuLinkManager; + /** + * The replica kill switch. + * + * @var \Drupal\Core\Database\ReplicaKillSwitch + */ + protected $replicaKillSwitch; + /** * The database connection. * @@ -43,11 +51,14 @@ class MenuRouterRebuildSubscriber implements EventSubscriberInterface { * The menu link plugin manager. * @param \Drupal\Core\Database\Connection $connection * The database connection. + * @param \Drupal\Core\Database\ReplicaKillSwitch $replica_kill_switch + * The replica kill switch. */ - public function __construct(LockBackendInterface $lock, MenuLinkManagerInterface $menu_link_manager, Connection $connection) { + public function __construct(LockBackendInterface $lock, MenuLinkManagerInterface $menu_link_manager, Connection $connection, ReplicaKillSwitch $replica_kill_switch) { $this->lock = $lock; $this->menuLinkManager = $menu_link_manager; $this->connection = $connection; + $this->replicaKillSwitch = $replica_kill_switch; } /** @@ -71,7 +82,7 @@ class MenuRouterRebuildSubscriber implements EventSubscriberInterface { // Ensure the menu links are up to date. $this->menuLinkManager->rebuild(); // Ignore any database replicas temporarily. - db_ignore_replica(); + $this->replicaKillSwitch->trigger(); } catch (\Exception $e) { $transaction->rollBack(); diff --git a/core/lib/Drupal/Core/EventSubscriber/ReplicaDatabaseIgnoreSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ReplicaDatabaseIgnoreSubscriber.php deleted file mode 100644 index fc123d36d1a..00000000000 --- a/core/lib/Drupal/Core/EventSubscriber/ReplicaDatabaseIgnoreSubscriber.php +++ /dev/null @@ -1,55 +0,0 @@ -= REQUEST_TIME) { - Database::ignoreTarget('default', 'replica'); - } - else { - unset($_SESSION['ignore_replica_server']); - } - } - } - - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents() { - $events[KernelEvents::REQUEST][] = ['checkReplicaServer']; - return $events; - } - -} diff --git a/core/tests/Drupal/KernelTests/Core/Database/DatabaseLegacyTest.php b/core/tests/Drupal/KernelTests/Core/Database/DatabaseLegacyTest.php index 3125da8cff7..dd80305f1e9 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/DatabaseLegacyTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/DatabaseLegacyTest.php @@ -488,4 +488,18 @@ class DatabaseLegacyTest extends DatabaseTestBase { $this->assertInstanceOf(Select::class, db_select('test')); } + /** + * Tests the db_ignore_replica() function. + * + * @expectedDeprecation db_ignore_replica() is deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.0. Use \Drupal\Core\Database\ReplicaKillSwitch::trigger() instead. See https://www.drupal.org/node/2997500 + */ + public function testDbIgnoreReplica() { + $connection = Database::getConnectionInfo('default'); + Database::addConnectionInfo('default', 'replica', $connection['default']); + db_ignore_replica(); + /** @var \Symfony\Component\HttpFoundation\Session\SessionInterface $session */ + $session = \Drupal::service('session'); + $this->assertTrue($session->has('ignore_replica_server')); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/EventSubscriber/IgnoreReplicaSubscriberTest.php b/core/tests/Drupal/KernelTests/Core/Database/ReplicaKillSwitchTest.php similarity index 60% rename from core/tests/Drupal/KernelTests/Core/EventSubscriber/IgnoreReplicaSubscriberTest.php rename to core/tests/Drupal/KernelTests/Core/Database/ReplicaKillSwitchTest.php index 237edb63c31..b2cdf72407b 100644 --- a/core/tests/Drupal/KernelTests/Core/EventSubscriber/IgnoreReplicaSubscriberTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/ReplicaKillSwitchTest.php @@ -1,24 +1,24 @@ trigger(); $class_loader = require $this->root . '/autoload.php'; $kernel = new DrupalKernel('testing', $class_loader, FALSE); $event = new GetResponseEvent($kernel, Request::create('http://example.com'), HttpKernelInterface::MASTER_REQUEST); - $subscriber = new ReplicaDatabaseIgnoreSubscriber(); - $subscriber->checkReplicaServer($event); + $service->checkReplicaServer($event); $db1 = Database::getConnection('default', 'default'); $db2 = Database::getConnection('replica', 'default'); $this->assertSame($db1, $db2, 'System Init ignores secondaries when requested.'); + + // Makes sure that session value set right. + $session = \Drupal::service('session'); + $this->assertTrue($session->has('ignore_replica_server')); + $expected = \Drupal::time()->getRequestTime() + Settings::get('maximum_replication_lag', 300); + $this->assertEquals($expected, $session->get('ignore_replica_server')); } }