From a25c727d9812ccdd325e90b7a7f1cfdd5a77e462 Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Wed, 23 Jun 2021 16:55:26 +0100
Subject: [PATCH] Issue #2124069 by voleger, andypost, martin107, damiankloip,
 cburschka, dawehner, kim.pepper, daffie, pcambra, ParisLiakos, Xano,
 longwave, MerryHamster, vacho, kostyashupenko, catch, alexpott, amateescu,
 Mile23, claudiu.cristea, larowlan, almaudoh, Berdir, zviryatko: Convert
 schema.inc to the update.update_hook_registry service (UpdateHookRegistry)

---
 core/core.services.yml                        |   8 +-
 core/includes/install.inc                     |   2 +-
 core/includes/schema.inc                      |  88 +++++-----
 core/includes/update.inc                      |  39 +++--
 .../Core/Extension/ModuleExtensionList.php    |   3 +-
 .../Drupal/Core/Extension/ModuleInstaller.php |  28 ++-
 .../Drupal/Core/Update/UpdateHookRegistry.php | 164 ++++++++++++++++++
 .../Core/Update/UpdateHookRegistryFactory.php |  28 +++
 core/lib/Drupal/Core/Updater/Module.php       |   2 +-
 .../update/drupal-8.action-3022401.php        |  12 ++
 .../src/Controller/DbUpdateController.php     |   2 +-
 .../system/src/Form/ModulesUninstallForm.php  |  22 ++-
 core/modules/system/system.install            |  14 +-
 .../src/Functional/Module/InstallTest.php     |   6 +-
 .../src/Functional/System/StatusTest.php      |   7 +-
 .../NoPreExistingSchemaUpdateTest.php         |   4 +-
 .../UpdatePathLastRemovedTest.php             |  16 +-
 .../UpdatePathNewDependencyTest.php           |   2 +-
 .../UpdatePostUpdateFailingTest.php           |  10 +-
 .../UpdateSystem/UpdatePostUpdateTest.php     |  10 +-
 .../UpdateRemovedPostUpdateTest.php           |  10 +-
 .../UpdateSystem/UpdateSchemaTest.php         |  10 +-
 .../UpdateSystem/UpdateScriptTest.php         |  37 ++--
 .../UpdateSystem/UpdatesWith7xTest.php        |   7 +-
 .../Kernel/Extension/ModuleHandlerTest.php    |  12 +-
 .../Update/UpdatePathTestBaseTest.php         |  10 +-
 .../ModuleInstallerDeprecationTest.php        |  28 +++
 .../Core/Extension/UpdateDeprecationTest.php  |  23 +++
 .../Core/Extension/UpdateDescriptionTest.php  |   2 +-
 .../Core/Extension/UpdateSchemaTest.php       |   4 +-
 .../Core/Update/UpdateHookRegistryTest.php    | 147 ++++++++++++++++
 .../Drupal/Tests/UpdatePathTestTrait.php      |  10 +-
 32 files changed, 608 insertions(+), 159 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Update/UpdateHookRegistry.php
 create mode 100644 core/lib/Drupal/Core/Update/UpdateHookRegistryFactory.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerDeprecationTest.php
 create mode 100644 core/tests/Drupal/Tests/Core/Update/UpdateHookRegistryTest.php

diff --git a/core/core.services.yml b/core/core.services.yml
index 6ac179594bc7..f1b8355e7937 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -527,7 +527,7 @@ services:
     class: Drupal\Core\Extension\ModuleInstaller
     tags:
       - { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator }
-    arguments: ['%app.root%', '@module_handler', '@kernel', '@database']
+    arguments: ['%app.root%', '@module_handler', '@kernel', '@database', '@update.update_hook_registry']
     lazy: true
   extension.list.module:
     class: Drupal\Core\Extension\ModuleExtensionList
@@ -1714,6 +1714,12 @@ services:
       - { name: placeholder_strategy, priority: -1000 }
   email.validator:
     class: Drupal\Component\Utility\EmailValidator
+  update.update_hook_registry:
+    class: Drupal\Core\Update\UpdateHookRegistry
+    factory: ['@update.update_hook_registry_factory', create]
+  update.update_hook_registry_factory:
+    class: Drupal\Core\Update\UpdateHookRegistryFactory
+    parent: container.trait
   update.post_update_registry:
     class: Drupal\Core\Update\UpdateRegistry
     factory: ['@update.post_update_registry_factory', create]
diff --git a/core/includes/install.inc b/core/includes/install.inc
index 3983773e81d4..e68522133271 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -81,7 +81,7 @@
 function drupal_load_updates() {
   /** @var \Drupal\Core\Extension\ModuleExtensionList $extension_list_module */
   $extension_list_module = \Drupal::service('extension.list.module');
-  foreach (drupal_get_installed_schema_version(NULL, FALSE, TRUE) as $module => $schema_version) {
+  foreach (\Drupal::service('update.update_hook_registry')->getAllInstalledVersions() as $module => $schema_version) {
     if ($extension_list_module->exists($module) && !$extension_list_module->checkIncompatibility($module)) {
       if ($schema_version > -1) {
         module_load_install($module);
diff --git a/core/includes/schema.inc b/core/includes/schema.inc
index 6026770c6a9e..62cc2024596a 100644
--- a/core/includes/schema.inc
+++ b/core/includes/schema.inc
@@ -5,6 +5,8 @@
  * Schema API handling functions.
  */
 
+use Drupal\Core\Update\UpdateHookRegistry;
+
 /**
  * @addtogroup schemaapi
  * @{
@@ -12,8 +14,14 @@
 
 /**
  * Indicates that a module has not been installed yet.
+ *
+ * @deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use
+ *   \Drupal\Core\Update\UpdateHookRegistry::SCHEMA_UNINSTALLED constant
+ *   instead.
+ *
+ * @see https://www.drupal.org/node/2444417
  */
-const SCHEMA_UNINSTALLED = -1;
+const SCHEMA_UNINSTALLED = UpdateHookRegistry::SCHEMA_UNINSTALLED;
 
 /**
  * Returns an array of available schema versions for a module.
@@ -24,37 +32,16 @@
  * @return array|bool
  *   If the module has updates, an array of available updates sorted by
  *   version. Otherwise, FALSE.
+ *
+ * @deprecated in drupal:9.3.0 and is removed from drupal:10.0.0.
+ *   Use \Drupal\Core\Update\SchemaDataInterface::getAvailableUpdates() instead.
+ *
+ * @see https://www.drupal.org/node/2444417
+ * @see \Drupal\Core\Update\UpdateHookRegistry::getAvailableUpdates()
  */
 function drupal_get_schema_versions($module) {
-  $updates = &drupal_static(__FUNCTION__, NULL);
-  if (!isset($updates[$module])) {
-    $updates = [];
-    foreach (\Drupal::moduleHandler()->getModuleList() as $loaded_module => $filename) {
-      $updates[$loaded_module] = [];
-    }
-
-    // Prepare regular expression to match all possible defined hook_update_N().
-    $regexp = '/^(?<module>.+)_update_(?<version>\d+)$/';
-    $functions = get_defined_functions();
-    // Narrow this down to functions ending with an integer, since all
-    // hook_update_N() functions end this way, and there are other
-    // possible functions which match '_update_'. We use preg_grep() here
-    // since looping through all PHP functions can take significant page
-    // execution time and this function is called on every administrative page
-    // via system_requirements().
-    foreach (preg_grep('/_\d+$/', $functions['user']) as $function) {
-      // If this function is a module update function, add it to the list of
-      // module updates.
-      if (preg_match($regexp, $function, $matches)) {
-        $updates[$matches['module']][] = (int) $matches['version'];
-      }
-    }
-    // Ensure that updates are applied in numerical order.
-    foreach ($updates as &$module_updates) {
-      sort($module_updates, SORT_NUMERIC);
-    }
-  }
-  return empty($updates[$module]) ? FALSE : $updates[$module];
+  @trigger_error('drupal_get_schema_versions() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use \Drupal\Core\Update\UpdateHookRegistry::getAvailableUpdates() instead. See https://www.drupal.org/node/2444417', E_USER_DEPRECATED);
+  return \Drupal::service('update.update_hook_registry')->getAvailableUpdates($module);
 }
 
 /**
@@ -71,26 +58,23 @@ function drupal_get_schema_versions($module) {
  * @return string|int
  *   The currently installed schema version, or SCHEMA_UNINSTALLED if the
  *   module is not installed.
+ *
+ * @deprecated in drupal:9.3.0 and is removed from drupal:10.0.0.
+ *   Use \Drupal\Core\Update\UpdateHookRegistry::getInstalledVersion() or
+ *   \Drupal\Core\Update\UpdateHookRegistry::getAllInstalledVersions()
+ *   instead.
+ *
+ * @see https://www.drupal.org/node/2444417
+ * @see \Drupal\Core\Update\UpdateHookRegistry::getInstalledVersion()
  */
 function drupal_get_installed_schema_version($module, $reset = FALSE, $array = FALSE) {
-  $versions = &drupal_static(__FUNCTION__, []);
-
-  if ($reset) {
-    $versions = [];
-  }
-
-  if (!$versions) {
-    if (!$versions = \Drupal::keyValue('system.schema')->getAll()) {
-      $versions = [];
-    }
-  }
-
+  @trigger_error('drupal_get_installed_schema_version() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use \Drupal\Core\Update\UpdateHookRegistry::getInstalledVersion() or \Drupal\Core\Update\UpdateHookRegistry::getAllInstalledVersions() instead. See https://www.drupal.org/node/2444417', E_USER_DEPRECATED);
+  /** @var \Drupal\Core\Update\UpdateHookRegistry $service */
+  $service = \Drupal::service('update.update_hook_registry');
   if ($array) {
-    return $versions;
-  }
-  else {
-    return isset($versions[$module]) ? $versions[$module] : SCHEMA_UNINSTALLED;
+    return $service->getAllInstalledVersions();
   }
+  return $service->getInstalledVersion((string) $module);
 }
 
 /**
@@ -100,11 +84,17 @@ function drupal_get_installed_schema_version($module, $reset = FALSE, $array = F
  *   A module name.
  * @param string $version
  *   The new schema version.
+ *
+ * @deprecated in drupal:9.3.0 and is removed from drupal:10.0.0.
+ *   Use \Drupal\Core\Update\UpdateHookRegistry::setInstalledVersion()
+ *   instead.
+ *
+ * @see https://www.drupal.org/node/2444417
+ * @see \Drupal\Core\Update\UpdateHookRegistry::setInstalledVersion()
  */
 function drupal_set_installed_schema_version($module, $version) {
-  \Drupal::keyValue('system.schema')->set($module, $version);
-  // Reset the static cache of module schema versions.
-  drupal_get_installed_schema_version(NULL, TRUE);
+  @trigger_error('drupal_set_installed_schema_version() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use \Drupal\Core\Update\UpdateHookRegistry::setInstalledVersion() instead. See https://www.drupal.org/node/2444417', E_USER_DEPRECATED);
+  \Drupal::service('update.update_hook_registry')->setInstalledVersion($module, $version);
 }
 
 /**
diff --git a/core/includes/update.inc b/core/includes/update.inc
index 77685bd5bfbf..09a6227fd659 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -57,7 +57,7 @@ function update_check_incompatibility($name, $type = 'module') {
 function update_system_schema_requirements() {
   $requirements = [];
 
-  $system_schema = drupal_get_installed_schema_version('system');
+  $system_schema = \Drupal::service('update.update_hook_registry')->getInstalledVersion('system');
 
   $requirements['minimum schema']['title'] = 'Minimum schema version';
   if ($system_schema >= \Drupal::CORE_MINIMUM_SCHEMA_VERSION) {
@@ -104,7 +104,9 @@ function update_check_requirements() {
  * https://www.drupal.org/project/drupal/issues/3130037 has been fixed.
  */
 function _update_fix_missing_schema() {
-  $versions = \Drupal::keyValue('system.schema')->getAll();
+  /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+  $update_registry = \Drupal::service('update.update_hook_registry');
+  $versions = $update_registry->getAllInstalledVersions();
   $module_handler = \Drupal::moduleHandler();
   $enabled_modules = $module_handler->getModuleList();
 
@@ -114,7 +116,7 @@ function _update_fix_missing_schema() {
     if (!isset($versions[$module])) {
       // Ensure the .install file is loaded.
       module_load_install($module);
-      $all_updates = drupal_get_schema_versions($module);
+      $all_updates = $update_registry->getAvailableUpdates($module);
       // If the schema version of a module hasn't been recorded, we cannot
       // know the actual schema version a module is at, because
       // no updates will ever have been run on the site and it was not set
@@ -136,7 +138,7 @@ function _update_fix_missing_schema() {
       if ($last_removed = $module_handler->invoke($module, 'update_last_removed')) {
         $last_update = max($last_update, $last_removed);
       }
-      drupal_set_installed_schema_version($module, $last_update);
+      $update_registry->setInstalledVersion($module, $last_update);
       $args = ['%module' => $module, '%last_update_hook' => $module . '_update_' . $last_update . '()'];
       \Drupal::messenger()->addWarning(t('Schema information for module %module was missing from the database. You should manually review the module updates and your database to check if any updates have been skipped up to, and including, %last_update_hook.', $args));
       \Drupal::logger('update')->warning('Schema information for module %module was missing from the database. You should manually review the module updates and your database to check if any updates have been skipped up to, and including, %last_update_hook.', $args);
@@ -161,12 +163,11 @@ function _update_fix_missing_schema() {
  */
 function update_set_schema($module, $schema_version) {
   @trigger_error('update_set_schema() is deprecated in drupal:9.2.0 and is removed from drupal:10.0.0. No direct replacement is provided. See https://www.drupal.org/node/3210925', E_USER_DEPRECATED);
-  \Drupal::keyValue('system.schema')->set($module, $schema_version);
+  \Drupal::service('update.update_hook_registry')->setInstalledVersion($module, $schema_version);
   \Drupal::service('extension.list.profile')->reset();
   \Drupal::service('extension.list.module')->reset();
   \Drupal::service('extension.list.theme_engine')->reset();
   \Drupal::service('extension.list.theme')->reset();
-  drupal_static_reset('drupal_get_installed_schema_version');
 }
 
 /**
@@ -262,7 +263,7 @@ function update_do_one($module, $number, $dependency_map, &$context) {
 
   // Record the schema update if it was completed successfully.
   if ($context['finished'] == 1 && empty($ret['#abort'])) {
-    drupal_set_installed_schema_version($module, $number);
+    \Drupal::service('update.update_hook_registry')->setInstalledVersion($module, $number);
   }
 
   $context['message'] = t('Updating @module', ['@module' => $module]);
@@ -351,7 +352,9 @@ function update_get_update_list() {
   // Make sure that the system module is first in the list of updates.
   $ret = ['system' => []];
 
-  $modules = drupal_get_installed_schema_version(NULL, FALSE, TRUE);
+  /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+  $update_registry = \Drupal::service('update.update_hook_registry');
+  $modules = $update_registry->getAllInstalledVersions();
   /** @var \Drupal\Core\Extension\ExtensionList $extension_list */
   $extension_list = \Drupal::service('extension.list.module');
   /** @var array $installed_module_info */
@@ -359,7 +362,7 @@ function update_get_update_list() {
   foreach ($modules as $module => $schema_version) {
     // Skip uninstalled and incompatible modules.
     try {
-      if ($schema_version == SCHEMA_UNINSTALLED || $extension_list->checkIncompatibility($module)) {
+      if ($schema_version == $update_registry::SCHEMA_UNINSTALLED || $extension_list->checkIncompatibility($module)) {
         continue;
       }
     }
@@ -393,8 +396,8 @@ function update_get_update_list() {
       continue;
     }
     // Otherwise, get the list of updates defined by this module.
-    $updates = drupal_get_schema_versions($module);
-    if ($updates !== FALSE) {
+    $updates = $update_registry->getAvailableUpdates($module);
+    if ($updates) {
       foreach ($updates as $update) {
         if ($update == \Drupal::CORE_MINIMUM_SCHEMA_VERSION) {
           $ret[$module]['warning'] = '<em>' . $module . '</em> module cannot be updated. It contains an update numbered as ' . \Drupal::CORE_MINIMUM_SCHEMA_VERSION . ' which is reserved for the earliest installation of a module in Drupal ' . \Drupal::CORE_COMPATIBILITY . ', before any updates. In order to update <em>' . $module . '</em> module, you will need to install a version of the module with valid updates.';
@@ -527,10 +530,12 @@ function update_get_update_function_list($starting_updates) {
   // Go through each module and find all updates that we need (including the
   // first update that was requested and any updates that run after it).
   $update_functions = [];
+  /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+  $update_registry = \Drupal::service('update.update_hook_registry');
   foreach ($starting_updates as $module => $version) {
     $update_functions[$module] = [];
-    $updates = drupal_get_schema_versions($module);
-    if ($updates !== FALSE) {
+    $updates = $update_registry->getAvailableUpdates($module);
+    if ($updates) {
       $max_version = max($updates);
       if ($version <= $max_version) {
         foreach ($updates as $update) {
@@ -659,7 +664,7 @@ function update_is_missing($module, $number, $update_functions) {
  *   performed; FALSE otherwise.
  */
 function update_already_performed($module, $number) {
-  return $number <= drupal_get_installed_schema_version($module);
+  return $number <= \Drupal::service('update.update_hook_registry')->getInstalledVersion($module);
 }
 
 /**
@@ -681,9 +686,11 @@ function update_retrieve_dependencies() {
   $return = [];
   /** @var \Drupal\Core\Extension\ModuleExtensionList */
   $extension_list = \Drupal::service('extension.list.module');
+  /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+  $update_registry = \Drupal::service('update.update_hook_registry');
   // Get a list of installed modules, arranged so that we invoke their hooks in
   // the same order that \Drupal::moduleHandler()->invokeAll() does.
-  foreach (\Drupal::keyValue('system.schema')->getAll() as $module => $schema) {
+  foreach ($update_registry->getAllInstalledVersions() as $module => $schema) {
     // Skip modules that are entirely missing from the filesystem here, since
     // module_load_install() will call trigger_error() if invoked on a module
     // that doesn't exist. There's no way to catch() that, so avoid it entirely.
@@ -691,7 +698,7 @@ function update_retrieve_dependencies() {
     // store for modules that have been removed from a site without first being
     // cleanly uninstalled. We don't care here if the module has been installed
     // or not, since we'll filter those out in update_get_update_list().
-    if ($schema == SCHEMA_UNINSTALLED || !$extension_list->exists($module)) {
+    if ($schema == $update_registry::SCHEMA_UNINSTALLED || !$extension_list->exists($module)) {
       // Nothing to upgrade.
       continue;
     }
diff --git a/core/lib/Drupal/Core/Extension/ModuleExtensionList.php b/core/lib/Drupal/Core/Extension/ModuleExtensionList.php
index c05d892b24bf..ab4be3869846 100644
--- a/core/lib/Drupal/Core/Extension/ModuleExtensionList.php
+++ b/core/lib/Drupal/Core/Extension/ModuleExtensionList.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\State\StateInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Update\UpdateHookRegistry;
 
 /**
  * Provides a list of available modules.
@@ -163,7 +164,7 @@ protected function doList() {
     foreach ($extensions as $name => $module) {
       $module->weight = isset($installed_modules[$name]) ? $installed_modules[$name] : 0;
       $module->status = (int) isset($installed_modules[$name]);
-      $module->schema_version = SCHEMA_UNINSTALLED;
+      $module->schema_version = UpdateHookRegistry::SCHEMA_UNINSTALLED;
     }
     $extensions = $this->moduleHandler->buildModuleDependencies($extensions);
 
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index 55d1385cb8df..83cf8bed678f 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Extension\Exception\ObsoleteExtensionException;
 use Drupal\Core\Installer\InstallerKernel;
 use Drupal\Core\Serialization\Yaml;
+use Drupal\Core\Update\UpdateHookRegistry;
 
 /**
  * Default implementation of the module installer.
@@ -53,6 +54,13 @@ class ModuleInstaller implements ModuleInstallerInterface {
    */
   protected $connection;
 
+  /**
+   * The update registry service.
+   *
+   * @var \Drupal\Core\Update\UpdateHookRegistry
+   */
+  protected $updateRegistry;
+
   /**
    * The uninstall validators.
    *
@@ -71,11 +79,13 @@ class ModuleInstaller implements ModuleInstallerInterface {
    *   The drupal kernel.
    * @param \Drupal\Core\Database\Connection $connection
    *   The database connection.
+   * @param \Drupal\Core\Update\UpdateHookRegistry|null $update_registry
+   *   (Optional) The update registry service.
    *
    * @see \Drupal\Core\DrupalKernel
    * @see \Drupal\Core\CoreServiceProvider
    */
-  public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel, Connection $connection = NULL) {
+  public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel, Connection $connection = NULL, UpdateHookRegistry $update_registry = NULL) {
     $this->root = $root;
     $this->moduleHandler = $module_handler;
     $this->kernel = $kernel;
@@ -84,6 +94,11 @@ public function __construct($root, ModuleHandlerInterface $module_handler, Drupa
       $connection = \Drupal::service('database');
     }
     $this->connection = $connection;
+    if (!$update_registry) {
+      @trigger_error('Calling ' . __METHOD__ . '() without the $update_registry argument is deprecated in drupal:9.3.0 and $update_registry argument will be required in drupal:10.0.0. See https://www.drupal.org/node/2124069', E_USER_DEPRECATED);
+      $update_registry = \Drupal::service('update.update_hook_registry');
+    }
+    $this->updateRegistry = $update_registry;
   }
 
   /**
@@ -254,7 +269,7 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
         // Set the schema version to the number of the last update provided by
         // the module, or the minimum core schema version.
         $version = \Drupal::CORE_MINIMUM_SCHEMA_VERSION;
-        $versions = drupal_get_schema_versions($module);
+        $versions = $this->updateRegistry->getAvailableUpdates($module);
         if ($versions) {
           $version = max(max($versions), $version);
         }
@@ -314,7 +329,7 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
         if ($last_removed = $this->moduleHandler->invoke($module, 'update_last_removed')) {
           $version = max($version, $last_removed);
         }
-        drupal_set_installed_schema_version($module, $version);
+        $this->updateRegistry->setInstalledVersion($module, $version);
 
         // Ensure that all post_update functions are registered already. This
         // should include existing post-updates, as well as any specified as
@@ -525,8 +540,9 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
 
       \Drupal::logger('system')->info('%module module uninstalled.', ['%module' => $module]);
 
-      $schema_store = \Drupal::keyValue('system.schema');
-      $schema_store->delete($module);
+      /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+      $update_registry = \Drupal::service('update.update_hook_registry');
+      $update_registry->deleteInstalledVersion($module);
 
       /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
       $post_update_registry = \Drupal::service('update.post_update_registry');
@@ -537,7 +553,6 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
     // fastCGI which executes ::destruct() after the Module uninstallation page
     // was sent already.
     \Drupal::service('router.builder')->rebuild();
-    drupal_get_installed_schema_version(NULL, TRUE);
 
     // Let other modules react.
     $this->moduleHandler->invokeAll('modules_uninstalled', [$module_list, $sync_status]);
@@ -605,6 +620,7 @@ protected function updateKernel($module_filenames) {
     $container = $this->kernel->getContainer();
     $this->moduleHandler = $container->get('module_handler');
     $this->connection = $container->get('database');
+    $this->updateRegistry = $container->get('update.update_hook_registry');
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Update/UpdateHookRegistry.php b/core/lib/Drupal/Core/Update/UpdateHookRegistry.php
new file mode 100644
index 000000000000..db5a8c43d6e3
--- /dev/null
+++ b/core/lib/Drupal/Core/Update/UpdateHookRegistry.php
@@ -0,0 +1,164 @@
+<?php
+
+namespace Drupal\Core\Update;
+
+use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
+
+/**
+ * Provides module updates versions handling.
+ */
+class UpdateHookRegistry {
+
+  /**
+   * Indicates that a module has not been installed yet.
+   */
+  public const SCHEMA_UNINSTALLED = -1;
+
+  /**
+   * A list of enabled modules.
+   *
+   * @var string[]
+   */
+  protected $enabledModules;
+
+  /**
+   * The key value storage.
+   *
+   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+   */
+  protected $keyValue;
+
+  /**
+   * A static cache of schema currentVersions per module.
+   *
+   * Stores schema versions of the modules based on their defined hook_update_N
+   * implementations.
+   * Example:
+   * ```
+   * [
+   *   'example_module' => [
+   *     8000,
+   *     8001,
+   *     8002
+   *   ]
+   * ]
+   * ```
+   *
+   * @var int[][]
+   * @see \Drupal\Core\Update\UpdateHookRegistry::getAvailableUpdates()
+   */
+  protected $allAvailableSchemaVersions = [];
+
+  /**
+   * Constructs a new UpdateRegistry.
+   *
+   * @param string[] $enabled_modules
+   *   A list of enabled modules.
+   * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value
+   *   The key value store.
+   */
+  public function __construct(array $enabled_modules, KeyValueStoreInterface $key_value) {
+    $this->enabledModules = $enabled_modules;
+    $this->keyValue = $key_value;
+  }
+
+  /**
+   * Returns an array of available schema versions for a module.
+   *
+   * @param string $module
+   *   A module name.
+   *
+   * @return int[]
+   *   An array of available updates sorted by version. Empty array returned if
+   *   no updates available.
+   */
+  public function getAvailableUpdates(string $module): array {
+    if (!isset($this->allAvailableSchemaVersions[$module])) {
+      $this->allAvailableSchemaVersions[$module] = [];
+
+      foreach ($this->enabledModules as $enabled_module) {
+        $this->allAvailableSchemaVersions[$enabled_module] = [];
+      }
+
+      // Prepare regular expression to match all possible defined
+      // hook_update_N().
+      $regexp = '/^(?<module>.+)_update_(?<version>\d+)$/';
+      $functions = get_defined_functions();
+      // Narrow this down to functions ending with an integer, since all
+      // hook_update_N() functions end this way, and there are other
+      // possible functions which match '_update_'. We use preg_grep() here
+      // since looping through all PHP functions can take significant page
+      // execution time and this function is called on every administrative page
+      // via system_requirements().
+      foreach (preg_grep('/_\d+$/', $functions['user']) as $function) {
+        // If this function is a module update function, add it to the list of
+        // module updates.
+        if (preg_match($regexp, $function, $matches)) {
+          $this->allAvailableSchemaVersions[$matches['module']][] = (int) $matches['version'];
+        }
+      }
+      // Ensure that updates are applied in numerical order.
+      array_walk(
+        $this->allAvailableSchemaVersions,
+        static function (&$module_updates) {
+          sort($module_updates, SORT_NUMERIC);
+        }
+      );
+    }
+
+    return $this->allAvailableSchemaVersions[$module];
+  }
+
+  /**
+   * Returns the currently installed schema version for a module.
+   *
+   * @param string $module
+   *   A module name.
+   *
+   * @return int
+   *   The currently installed schema version, or self::SCHEMA_UNINSTALLED if the
+   *   module is not installed.
+   */
+  public function getInstalledVersion(string $module): int {
+    return $this->keyValue->get($module, self::SCHEMA_UNINSTALLED);
+  }
+
+  /**
+   * Updates the installed version information for a module.
+   *
+   * @param string $module
+   *   A module name.
+   * @param int $version
+   *   The new schema version.
+   *
+   * @return self
+   *   Returns self to support chained method calls.
+   */
+  public function setInstalledVersion(string $module, int $version): self {
+    $this->keyValue->set($module, $version);
+    return $this;
+  }
+
+  /**
+   * Deletes the installed version information for the module.
+   *
+   * @param string $module
+   *   The module name to delete.
+   */
+  public function deleteInstalledVersion(string $module): void {
+    $this->keyValue->delete($module);
+  }
+
+  /**
+   * Returns the currently installed schema version for all modules.
+   *
+   * @return int[]
+   *   Array of modules as the keys and values as the currently installed
+   *   schema version of corresponding module, or self::SCHEMA_UNINSTALLED if the
+   *   module is not installed.
+   */
+  public function getAllInstalledVersions(): array {
+    return $this->keyValue->getAll();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Update/UpdateHookRegistryFactory.php b/core/lib/Drupal/Core/Update/UpdateHookRegistryFactory.php
new file mode 100644
index 000000000000..45ac2b8d7c7d
--- /dev/null
+++ b/core/lib/Drupal/Core/Update/UpdateHookRegistryFactory.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\Core\Update;
+
+use Symfony\Component\DependencyInjection\ContainerAwareInterface;
+use Symfony\Component\DependencyInjection\ContainerAwareTrait;
+
+/**
+ * Service factory for the versioning update registry.
+ */
+class UpdateHookRegistryFactory implements ContainerAwareInterface {
+
+  use ContainerAwareTrait;
+
+  /**
+   * Creates a new UpdateHookRegistry instance.
+   *
+   * @return \Drupal\Core\Update\UpdateHookRegistry
+   *   The update registry instance.
+   */
+  public function create() {
+    return new UpdateHookRegistry(
+      array_keys($this->container->get('module_handler')->getModuleList()),
+      $this->container->get('keyvalue')->get('system.schema')
+    );
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Updater/Module.php b/core/lib/Drupal/Core/Updater/Module.php
index 69330cef9097..a50eda12bf26 100644
--- a/core/lib/Drupal/Core/Updater/Module.php
+++ b/core/lib/Drupal/Core/Updater/Module.php
@@ -89,7 +89,7 @@ public function getSchemaUpdates() {
     }
     module_load_include('install', $this->name);
 
-    if (!$updates = drupal_get_schema_versions($this->name)) {
+    if (!\Drupal::service('update.update_hook_registry')->getAvailableUpdates($this->name)) {
       return [];
     }
     $modules_with_updates = update_get_update_list();
diff --git a/core/modules/action/tests/fixtures/update/drupal-8.action-3022401.php b/core/modules/action/tests/fixtures/update/drupal-8.action-3022401.php
index bfebb360eaa8..7c73a36bb2ed 100644
--- a/core/modules/action/tests/fixtures/update/drupal-8.action-3022401.php
+++ b/core/modules/action/tests/fixtures/update/drupal-8.action-3022401.php
@@ -39,3 +39,15 @@
   ])
   ->condition('name', 'core.extension')
   ->execute();
+$connection->insert('key_value')
+  ->fields([
+    'collection',
+    'name',
+    'value',
+  ])
+  ->values([
+    'collection' => 'system.schema',
+    'name' => 'action',
+    'value' => serialize(8000),
+  ])
+  ->execute();
diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php
index 05af92706f8c..1732660b24cf 100644
--- a/core/modules/system/src/Controller/DbUpdateController.php
+++ b/core/modules/system/src/Controller/DbUpdateController.php
@@ -605,7 +605,7 @@ protected function triggerBatch(Request $request) {
         // correct place. (The updates are already sorted, so we can simply base
         // this on the first one we come across in the above foreach loop.)
         if (isset($start[$update['module']])) {
-          drupal_set_installed_schema_version($update['module'], $update['number'] - 1);
+          \Drupal::service('update.update_hook_registry')->setInstalledVersion($update['module'], $update['number'] - 1);
           unset($start[$update['module']]);
         }
         $operations[] = ['update_do_one', [$update['module'], $update['number'], $dependency_map[$function]]];
diff --git a/core/modules/system/src/Form/ModulesUninstallForm.php b/core/modules/system/src/Form/ModulesUninstallForm.php
index 52f378e70941..3d2c70b61c87 100644
--- a/core/modules/system/src/Form/ModulesUninstallForm.php
+++ b/core/modules/system/src/Form/ModulesUninstallForm.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
+use Drupal\Core\Update\UpdateHookRegistry;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -45,6 +46,13 @@ class ModulesUninstallForm extends FormBase {
    */
   protected $moduleExtensionList;
 
+  /**
+   * The update registry service.
+   *
+   * @var \Drupal\Core\Update\UpdateHookRegistry
+   */
+  protected $updateRegistry;
+
   /**
    * {@inheritdoc}
    */
@@ -53,7 +61,8 @@ public static function create(ContainerInterface $container) {
       $container->get('module_handler'),
       $container->get('module_installer'),
       $container->get('keyvalue.expirable')->get('modules_uninstall'),
-      $container->get('extension.list.module')
+      $container->get('extension.list.module'),
+      $container->get('update.update_hook_registry')
     );
   }
 
@@ -68,12 +77,19 @@ public static function create(ContainerInterface $container) {
    *   The key value expirable factory.
    * @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
    *   The module extension list.
+   * @param \Drupal\Core\Update\UpdateHookRegistry|null $versioning_update_registry
+   *   Versioning update registry service.
    */
-  public function __construct(ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, KeyValueStoreExpirableInterface $key_value_expirable, ModuleExtensionList $extension_list_module) {
+  public function __construct(ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, KeyValueStoreExpirableInterface $key_value_expirable, ModuleExtensionList $extension_list_module, UpdateHookRegistry $versioning_update_registry = NULL) {
     $this->moduleExtensionList = $extension_list_module;
     $this->moduleHandler = $module_handler;
     $this->moduleInstaller = $module_installer;
     $this->keyValueExpirable = $key_value_expirable;
+    if ($versioning_update_registry === NULL) {
+      @trigger_error('The update.update_hook_registry service must be passed to ' . __NAMESPACE__ . '\ModulesUninstallForm::__construct(). It was added in drupal:9.3.0 and will be required before drupal:10.0.0.', E_USER_DEPRECATED);
+      $versioning_update_registry = \Drupal::service('update.update_hook_registry');
+    }
+    $this->updateRegistry = $versioning_update_registry;
   }
 
   /**
@@ -153,7 +169,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       // All modules which depend on this one must be uninstalled first, before
       // we can allow this module to be uninstalled.
       foreach (array_keys($module->required_by) as $dependent) {
-        if (drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED) {
+        if ($this->updateRegistry->getInstalledVersion($dependent) !== $this->updateRegistry::SCHEMA_UNINSTALLED) {
           $form['modules'][$module->getName()]['#required_by'][] = $dependent;
           $form['uninstall'][$module->getName()]['#disabled'] = TRUE;
         }
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index cf83bd4d044e..f0a4f317be3f 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -789,10 +789,12 @@ function system_requirements($phase) {
 
     // Check installed modules.
     $has_pending_updates = FALSE;
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
     foreach (\Drupal::moduleHandler()->getModuleList() as $module => $filename) {
-      $updates = drupal_get_schema_versions($module);
-      if ($updates !== FALSE) {
-        $default = drupal_get_installed_schema_version($module);
+      $updates = $update_registry->getAvailableUpdates($module);
+      if ($updates) {
+        $default = $update_registry->getInstalledVersion($module);
         if (max($updates) > $default) {
           $has_pending_updates = TRUE;
           break;
@@ -1224,17 +1226,19 @@ function system_requirements($phase) {
   // one that was last removed.
   if ($phase == 'update') {
     $module_handler = \Drupal::moduleHandler();
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
     $module_list = [];
     foreach ($module_handler->getImplementations('update_last_removed') as $module) {
       $last_removed = $module_handler->invoke($module, 'update_last_removed');
-      if ($last_removed && $last_removed > drupal_get_installed_schema_version($module)) {
+      if ($last_removed && $last_removed > $update_registry->getInstalledVersion($module)) {
 
         /** @var \Drupal\Core\Extension\Extension $module_info */
         $module_info = \Drupal::service('extension.list.module')->get($module);
         $module_list[$module] = [
           'name' => $module_info->info['name'],
           'last_removed' => $last_removed,
-          'installed_version' => drupal_get_installed_schema_version($module),
+          'installed_version' => $update_registry->getInstalledVersion($module),
         ];
       }
     }
diff --git a/core/modules/system/tests/src/Functional/Module/InstallTest.php b/core/modules/system/tests/src/Functional/Module/InstallTest.php
index ab9cacb222d7..0bc323ea291b 100644
--- a/core/modules/system/tests/src/Functional/Module/InstallTest.php
+++ b/core/modules/system/tests/src/Functional/Module/InstallTest.php
@@ -51,9 +51,11 @@ public function testEnableUserTwice() {
    * Tests recorded schema versions of early installed modules in the installer.
    */
   public function testRequiredModuleSchemaVersions() {
-    $version = drupal_get_installed_schema_version('system', TRUE);
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
+    $version = $update_registry->getInstalledVersion('system');
     $this->assertGreaterThan(0, $version);
-    $version = drupal_get_installed_schema_version('user', TRUE);
+    $version = $update_registry->getInstalledVersion('user');
     $this->assertGreaterThan(0, $version);
 
     $post_update_key_value = \Drupal::keyValue('post_update');
diff --git a/core/modules/system/tests/src/Functional/System/StatusTest.php b/core/modules/system/tests/src/Functional/System/StatusTest.php
index cab94ed7ccd4..717c4aa56e16 100644
--- a/core/modules/system/tests/src/Functional/System/StatusTest.php
+++ b/core/modules/system/tests/src/Functional/System/StatusTest.php
@@ -65,14 +65,17 @@ public function testStatusPage() {
     // The setting config_sync_directory is not properly formed.
     $this->assertRaw(t("Your %file file must define the %setting setting", ['%file' => $this->siteDirectory . '/settings.php', '%setting' => "\$settings['config_sync_directory']"]));
 
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
+
     // Set the schema version of update_test_postupdate to a lower version, so
     // update_test_postupdate_update_8001() needs to be executed.
-    drupal_set_installed_schema_version('update_test_postupdate', 8000);
+    $update_registry->setInstalledVersion('update_test_postupdate', 8000);
     $this->drupalGet('admin/reports/status');
     $this->assertSession()->pageTextContains('Out of date');
 
     // Now cleanup the executed post update functions.
-    drupal_set_installed_schema_version('update_test_postupdate', 8001);
+    $update_registry->setInstalledVersion('update_test_postupdate', 8001);
     /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
     $post_update_registry = \Drupal::service('update.post_update_registry');
     $post_update_registry->filterOutInvokedUpdatesByModule('update_test_postupdate');
diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/NoPreExistingSchemaUpdateTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/NoPreExistingSchemaUpdateTest.php
index 0f889155d488..097341901c8f 100644
--- a/core/modules/system/tests/src/Functional/UpdateSystem/NoPreExistingSchemaUpdateTest.php
+++ b/core/modules/system/tests/src/Functional/UpdateSystem/NoPreExistingSchemaUpdateTest.php
@@ -45,7 +45,7 @@ protected function setUp(): void {
    * Tests the system module updates with no dependencies installed.
    */
   public function testNoPreExistingSchema() {
-    $schema = \Drupal::keyValue('system.schema')->getAll();
+    $schema = \Drupal::service('update.update_hook_registry')->getAllInstalledVersions();
     $this->assertArrayNotHasKey('update_test_no_preexisting', $schema);
     $this->assertFalse(\Drupal::state()->get('update_test_no_preexisting_update_8001', FALSE));
 
@@ -65,7 +65,7 @@ public function testNoPreExistingSchema() {
     $this->drupalGet($update_url);
     $this->updateRequirementsProblem();
 
-    $schema = \Drupal::keyValue('system.schema')->getAll();
+    $schema = \Drupal::service('update.update_hook_registry')->getAllInstalledVersions();
     $this->assertArrayHasKey('update_test_no_preexisting', $schema);
     $this->assertEquals('8001', $schema['update_test_no_preexisting']);
     // The schema version has been fixed, but the update was never run.
diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePathLastRemovedTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePathLastRemovedTest.php
index 78d08581921c..40d5d1e57324 100644
--- a/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePathLastRemovedTest.php
+++ b/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePathLastRemovedTest.php
@@ -55,8 +55,11 @@ protected function setUp(): void {
    * Tests that a module with a too old schema version can not be updated.
    */
   public function testLastRemovedVersion() {
-    drupal_set_installed_schema_version('update_test_last_removed', 8000);
-    drupal_set_installed_schema_version('system', 8804);
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
+
+    $update_registry->setInstalledVersion('update_test_last_removed', 8000);
+    $update_registry->setInstalledVersion('system', 8804);
 
     // Access the update page with a schema version that is too old for system
     // and the test module, only the generic core message should be shown.
@@ -72,7 +75,7 @@ public function testLastRemovedVersion() {
 
     // Update the installed version of system and then assert that now,
     // the test module is shown instead.
-    drupal_set_installed_schema_version('system', 8805);
+    $update_registry->setInstalledVersion('system', 8805);
     $this->drupalGet($this->updateUrl);
 
     $assert_session->pageTextNotContains('The version of Drupal you are trying to update from is too old');
@@ -83,11 +86,12 @@ public function testLastRemovedVersion() {
 
     // Set the expected schema version for the node and test module, updates are
     // successful now.
-    drupal_set_installed_schema_version('update_test_last_removed', 8002);
+    $update_registry->setInstalledVersion('update_test_last_removed', 8002);
 
     $this->runUpdates();
-    $this->assertEquals(8003, drupal_get_installed_schema_version('update_test_last_removed'));
-
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
+    $this->assertEquals(8003, $update_registry->getInstalledVersion('update_test_last_removed'));
   }
 
 }
diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePathNewDependencyTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePathNewDependencyTest.php
index d126d91609ab..8a2233c8063b 100644
--- a/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePathNewDependencyTest.php
+++ b/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePathNewDependencyTest.php
@@ -31,7 +31,7 @@ public function testUpdateNewDependency() {
       ->set('module.new_dependency_test', 0)
       ->set('module', module_config_sort($extension_config->get('module')))
       ->save(TRUE);
-    drupal_set_installed_schema_version('new_dependency_test', \Drupal::CORE_MINIMUM_SCHEMA_VERSION);
+    \Drupal::service('update.update_hook_registry')->setInstalledVersion('new_dependency_test', \Drupal::CORE_MINIMUM_SCHEMA_VERSION);
 
     // Rebuild the container and test that the service with the optional unmet
     // dependency is still available while the ones that fail are not.
diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePostUpdateFailingTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePostUpdateFailingTest.php
index d42df7d9a919..d5b678dc160d 100644
--- a/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePostUpdateFailingTest.php
+++ b/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePostUpdateFailingTest.php
@@ -27,15 +27,7 @@ protected function setUp(): void {
     $connection = Database::getConnection();
 
     // Set the schema version.
-    $connection->merge('key_value')
-      ->condition('collection', 'system.schema')
-      ->condition('name', 'update_test_failing')
-      ->fields([
-        'collection' => 'system.schema',
-        'name' => 'update_test_failing',
-        'value' => 'i:8000;',
-      ])
-      ->execute();
+    \Drupal::service('update.update_hook_registry')->setInstalledVersion('update_test_failing', 8000);
 
     // Update core.extension.
     $extensions = $connection->select('config')
diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePostUpdateTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePostUpdateTest.php
index bebc7247a7a2..25c5a89b15f1 100644
--- a/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePostUpdateTest.php
+++ b/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePostUpdateTest.php
@@ -28,15 +28,7 @@ protected function setUp(): void {
     $connection = Database::getConnection();
 
     // Set the schema version.
-    $connection->merge('key_value')
-      ->condition('collection', 'system.schema')
-      ->condition('name', 'update_test_postupdate')
-      ->fields([
-        'collection' => 'system.schema',
-        'name' => 'update_test_postupdate',
-        'value' => 'i:8000;',
-      ])
-      ->execute();
+    \Drupal::service('update.update_hook_registry')->setInstalledVersion('update_test_postupdate', 8000);
 
     // Update core.extension.
     $extensions = $connection->select('config')
diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/UpdateRemovedPostUpdateTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateRemovedPostUpdateTest.php
index c58aa5303b6c..8809c9514756 100644
--- a/core/modules/system/tests/src/Functional/UpdateSystem/UpdateRemovedPostUpdateTest.php
+++ b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateRemovedPostUpdateTest.php
@@ -28,15 +28,7 @@ protected function setUp(): void {
     $connection = Database::getConnection();
 
     // Set the schema version.
-    $connection->merge('key_value')
-      ->condition('collection', 'system.schema')
-      ->condition('name', 'update_test_postupdate')
-      ->fields([
-        'collection' => 'system.schema',
-        'name' => 'update_test_postupdate',
-        'value' => 'i:8000;',
-      ])
-      ->execute();
+    \Drupal::service('update.update_hook_registry')->setInstalledVersion('update_test_postupdate', 8000);
 
     // Update core.extension.
     $extensions = $connection->select('config')
diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/UpdateSchemaTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateSchemaTest.php
index 08f60f71964c..c39a8d99a1ef 100644
--- a/core/modules/system/tests/src/Functional/UpdateSystem/UpdateSchemaTest.php
+++ b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateSchemaTest.php
@@ -58,8 +58,11 @@ protected function setUp(): void {
   public function testUpdateHooks() {
     $connection = Database::getConnection();
 
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
+
     // Verify that the 8000 schema is in place.
-    $this->assertEquals(8000, drupal_get_installed_schema_version('update_test_schema'));
+    $this->assertEquals(8000, $update_registry->getInstalledVersion('update_test_schema'));
     $this->assertFalse($connection->schema()->indexExists('update_test_schema_table', 'test'), 'Version 8000 of the update_test_schema module is installed.');
 
     // Increment the schema version.
@@ -75,7 +78,10 @@ public function testUpdateHooks() {
     $this->checkForMetaRefresh();
 
     // Ensure schema has changed.
-    $this->assertEquals(8001, drupal_get_installed_schema_version('update_test_schema', TRUE));
+    $this->resetAll();
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
+    $this->assertEquals(8001, $update_registry->getInstalledVersion('update_test_schema'));
     // Ensure the index was added for column a.
     $this->assertTrue($connection->schema()->indexExists('update_test_schema_table', 'test'), 'Version 8001 of the update_test_schema module is installed.');
   }
diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php
index 401c01e8f73b..76d14154edab 100644
--- a/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php
+++ b/core/modules/system/tests/src/Functional/UpdateSystem/UpdateScriptTest.php
@@ -142,7 +142,9 @@ public function testRequirements() {
     // successfully.
     $this->drupalLogin($this->updateUser);
     $update_script_test_config->set('requirement_type', REQUIREMENT_WARNING)->save();
-    drupal_set_installed_schema_version('update_script_test', drupal_get_installed_schema_version('update_script_test') - 1);
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
+    $update_registry->setInstalledVersion('update_script_test', $update_registry->getInstalledVersion('update_script_test') - 1);
     $this->drupalGet($this->updateUrl, ['external' => TRUE]);
     $this->assertSession()->pageTextContains('This is a requirements warning provided by the update_script_test module.');
     $this->clickLink('try again');
@@ -410,7 +412,7 @@ public function testOrphanedSchemaEntries() {
     // nonexistent module. This replicates what would happen if you had a module
     // installed and then completely remove it from the filesystem and clear it
     // out of the core.extension config list without uninstalling it cleanly.
-    \Drupal::keyValue('system.schema')->set('my_already_removed_module', 8000);
+    \Drupal::service('update.update_hook_registry')->setInstalledVersion('my_already_removed_module', 8000);
 
     // Visit update.php and make sure we can click through to the 'No pending
     // updates' page without errors.
@@ -424,8 +426,8 @@ public function testOrphanedSchemaEntries() {
 
     // Try again with another orphaned entry, this time for a test module that
     // does exist in the filesystem.
-    \Drupal::keyValue('system.schema')->delete('my_already_removed_module');
-    \Drupal::keyValue('system.schema')->set('update_test_0', 8000);
+    \Drupal::service('update.update_hook_registry')->deleteInstalledVersion('my_already_removed_module');
+    \Drupal::service('update.update_hook_registry')->setInstalledVersion('update_test_0', 8000);
     $this->drupalGet($this->updateUrl, ['external' => TRUE]);
     $this->updateRequirementsProblem();
     $this->clickLink(t('Continue'));
@@ -435,7 +437,7 @@ public function testOrphanedSchemaEntries() {
     $this->assertSession()->elementTextEquals('xpath', '//div[@aria-label="Warning message"]', 'Warning message Module update_test_0 has an entry in the system.schema key/value storage, but is not installed. More information about this error.');
 
     // Finally, try with both kinds of orphans and make sure we get both warnings.
-    \Drupal::keyValue('system.schema')->set('my_already_removed_module', 8000);
+    \Drupal::service('update.update_hook_registry')->setInstalledVersion('my_already_removed_module', 8000);
     $this->drupalGet($this->updateUrl, ['external' => TRUE]);
     $this->updateRequirementsProblem();
     $this->clickLink(t('Continue'));
@@ -538,12 +540,15 @@ public function testSuccessfulUpdateFunctionality() {
     $this->assertEquals($initial_maintenance_mode, $final_maintenance_mode, 'Maintenance mode should not have changed after database updates.');
 
     // Reset the static cache to ensure we have the most current setting.
-    $schema_version = drupal_get_installed_schema_version('update_script_test', TRUE);
+    $this->resetAll();
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
+    $schema_version = $update_registry->getInstalledVersion('update_script_test');
     $this->assertEquals(8001, $schema_version, 'update_script_test schema version is 8001 after updating.');
 
     // Set the installed schema version to one less than the current update.
-    drupal_set_installed_schema_version('update_script_test', $schema_version - 1);
-    $schema_version = drupal_get_installed_schema_version('update_script_test', TRUE);
+    $update_registry->setInstalledVersion('update_script_test', $schema_version - 1);
+    $schema_version = $update_registry->getInstalledVersion('update_script_test');
     $this->assertEquals(8000, $schema_version, 'update_script_test schema version overridden to 8000.');
 
     // Click through update.php with 'access administration pages' and
@@ -603,12 +608,14 @@ public function testSuccessfulMultilingualUpdateFunctionality() {
     $config->save();
 
     // Reset the static cache to ensure we have the most current setting.
-    $schema_version = drupal_get_installed_schema_version('update_script_test', TRUE);
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
+    $schema_version = $update_registry->getInstalledVersion('update_script_test');
     $this->assertEquals(8001, $schema_version, 'update_script_test schema version is 8001 after updating.');
 
     // Set the installed schema version to one less than the current update.
-    drupal_set_installed_schema_version('update_script_test', $schema_version - 1);
-    $schema_version = drupal_get_installed_schema_version('update_script_test', TRUE);
+    $update_registry->setInstalledVersion('update_script_test', $schema_version - 1);
+    $schema_version = $update_registry->getInstalledVersion('update_script_test');
     $this->assertEquals(8000, $schema_version, 'update_script_test schema version overridden to 8000.');
 
     // Create admin user.
@@ -672,12 +679,14 @@ public function testMaintenanceModeLink() {
    * Helper function to run updates via the browser.
    */
   protected function runUpdates($maintenance_mode) {
-    $schema_version = drupal_get_installed_schema_version('update_script_test');
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
+    $schema_version = $update_registry->getInstalledVersion('update_script_test');
     $this->assertEquals(8001, $schema_version, 'update_script_test is initially installed with schema version 8001.');
 
     // Set the installed schema version to one less than the current update.
-    drupal_set_installed_schema_version('update_script_test', $schema_version - 1);
-    $schema_version = drupal_get_installed_schema_version('update_script_test', TRUE);
+    $update_registry->setInstalledVersion('update_script_test', $schema_version - 1);
+    $schema_version = $update_registry->getInstalledVersion('update_script_test');
     $this->assertEquals(8000, $schema_version, 'update_script_test schema version overridden to 8000.');
 
     // Click through update.php with 'administer software updates' permission.
diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/UpdatesWith7xTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/UpdatesWith7xTest.php
index 513bbfb7cbcb..37dace4d3b23 100644
--- a/core/modules/system/tests/src/Functional/UpdateSystem/UpdatesWith7xTest.php
+++ b/core/modules/system/tests/src/Functional/UpdateSystem/UpdatesWith7xTest.php
@@ -49,13 +49,16 @@ protected function setUp(): void {
   }
 
   public function testWith7x() {
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
+
     // Ensure that the minimum schema version is 8000, despite 7200 update
     // hooks and a 7XXX hook_update_last_removed().
-    $this->assertEquals(8000, drupal_get_installed_schema_version('update_test_with_7x'));
+    $this->assertEquals(8000, $update_registry->getInstalledVersion('update_test_with_7x'));
 
     // Try to manually set the schema version to 7110 and ensure that no
     // updates are allowed.
-    drupal_set_installed_schema_version('update_test_with_7x', 7110);
+    $update_registry->setInstalledVersion('update_test_with_7x', 7110);
 
     // Click through update.php with 'administer software updates' permission.
     $this->drupalLogin($this->updateUser);
diff --git a/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php b/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php
index d3411dd881e0..9d588a23e171 100644
--- a/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php
+++ b/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php
@@ -125,8 +125,10 @@ public function testDependencyResolution() {
     $result = $this->moduleInstaller()->uninstall(['config', 'help', 'color']);
     $this->assertTrue($result, 'ModuleInstaller::uninstall() returned TRUE.');
 
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
     foreach (['color', 'config', 'help'] as $module) {
-      $this->assertEquals(SCHEMA_UNINSTALLED, drupal_get_installed_schema_version($module), "{$module} module was uninstalled.");
+      $this->assertEquals($update_registry::SCHEMA_UNINSTALLED, $update_registry->getInstalledVersion($module), "{$module} module was uninstalled.");
     }
     $uninstalled_modules = \Drupal::state()->get('module_test.uninstall_order', []);
     $this->assertEquals(['color', 'config', 'help'], $uninstalled_modules, 'Modules were uninstalled in the correct order.');
@@ -176,10 +178,12 @@ public function testUninstallProfileDependency() {
     $this->assertTrue($this->moduleHandler()->moduleExists($dependency));
 
     // Uninstall the profile module that is not a dependent.
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
     $result = $this->moduleInstaller()->uninstall([$non_dependency]);
     $this->assertTrue($result, 'ModuleInstaller::uninstall() returns TRUE.');
     $this->assertFalse($this->moduleHandler()->moduleExists($non_dependency));
-    $this->assertEquals(SCHEMA_UNINSTALLED, drupal_get_installed_schema_version($non_dependency), "$non_dependency module was uninstalled.");
+    $this->assertEquals($update_registry::SCHEMA_UNINSTALLED, $update_registry->getInstalledVersion($non_dependency), "$non_dependency module was uninstalled.");
 
     // Verify that the installation profile itself was not uninstalled.
     $uninstalled_modules = \Drupal::state()->get('module_test.uninstall_order', []);
@@ -272,9 +276,11 @@ public function testUninstallContentDependency() {
     // Deleting the entity.
     $entity->delete();
 
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
     $result = $this->moduleInstaller()->uninstall(['help']);
     $this->assertTrue($result, 'ModuleInstaller::uninstall() returns TRUE.');
-    $this->assertEquals(SCHEMA_UNINSTALLED, drupal_get_installed_schema_version('entity_test'), "entity_test module was uninstalled.");
+    $this->assertEquals($update_registry::SCHEMA_UNINSTALLED, $update_registry->getInstalledVersion('entity_test'), "entity_test module was uninstalled.");
   }
 
   /**
diff --git a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php
index 925e18922c38..002749547e8c 100644
--- a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php
+++ b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php
@@ -34,8 +34,10 @@ public function testDatabaseLoaded() {
     // Set a value in the cache to prove caches are cleared.
     \Drupal::service('cache.default')->set(__CLASS__, 'Test');
 
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
     foreach (['user' => 8100, 'node' => 8700, 'system' => 8901, 'update_test_schema' => 8000] as $module => $schema) {
-      $this->assertEquals($schema, drupal_get_installed_schema_version($module), new FormattableMarkup('Module @module schema is @schema', ['@module' => $module, '@schema' => $schema]));
+      $this->assertEquals($schema, $update_registry->getInstalledVersion($module), new FormattableMarkup('Module @module schema is @schema', ['@module' => $module, '@schema' => $schema]));
     }
 
     // Ensure that all {router} entries can be unserialized. If they cannot be
@@ -100,8 +102,10 @@ public function testUpdateHookN() {
     $this->assertEquals([], $container_cannot_be_saved_messages);
 
     // Ensure schema has changed.
-    $this->assertEquals(8001, drupal_get_installed_schema_version('update_test_schema', TRUE));
-    $this->assertEquals(8001, drupal_get_installed_schema_version('update_test_semver_update_n', TRUE));
+    /** @var \Drupal\Core\Update\UpdateHookRegistry $update_registry */
+    $update_registry = \Drupal::service('update.update_hook_registry');
+    $this->assertEquals(8001, $update_registry->getInstalledVersion('update_test_schema'));
+    $this->assertEquals(8001, $update_registry->getInstalledVersion('update_test_semver_update_n'));
     // Ensure the index was added for column a.
     $this->assertTrue($connection->schema()->indexExists('update_test_schema_table', 'test'), 'Version 8001 of the update_test_schema module is installed.');
     // Ensure update_test_semver_update_n_update_8001 was run.
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerDeprecationTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerDeprecationTest.php
new file mode 100644
index 000000000000..2555fb518dcd
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerDeprecationTest.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Extension;
+
+use Drupal\Core\DrupalKernelInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Extension\ModuleInstaller;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @group legacy
+ * @group extension
+ * @coversDefaultClass \Drupal\Core\Extension\ModuleInstaller
+ */
+class ModuleInstallerDeprecationTest extends KernelTestBase {
+
+  /**
+   * @covers ::__construct
+   */
+  public function testConstructorDeprecation() {
+    $this->expectDeprecation('Calling ' . ModuleInstaller::class . '::__construct() without the $update_registry argument is deprecated in drupal:9.3.0 and $update_registry argument will be required in drupal:10.0.0. See https://www.drupal.org/node/2124069');
+    $root = '';
+    $module_handler = $this->prophesize(ModuleHandlerInterface::class);
+    $kernel = $this->prophesize(DrupalKernelInterface::class);
+    $this->assertNotNull(new ModuleInstaller($root, $module_handler->reveal(), $kernel->reveal()));
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/UpdateDeprecationTest.php b/core/tests/Drupal/KernelTests/Core/Extension/UpdateDeprecationTest.php
index 6e6f8584a4ea..21f739d4a970 100644
--- a/core/tests/Drupal/KernelTests/Core/Extension/UpdateDeprecationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Extension/UpdateDeprecationTest.php
@@ -43,4 +43,27 @@ public function testUpdateSetSchema() {
     $this->assertEquals(8003, \Drupal::keyValue('system.schema')->get('update_test_schema'));
   }
 
+  /**
+   * Deprecation testing for drupal_get_schema_versions function.
+   *
+   * @see drupal_get_schema_versions()
+   */
+  public function testDrupalGetSchemaVersionsLegacyTest() {
+    $this->expectDeprecation('drupal_get_schema_versions() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use \Drupal\Core\Update\UpdateHookRegistry::getAvailableUpdates() instead. See https://www.drupal.org/node/2444417');
+    $this->assertEmpty(drupal_get_schema_versions('update_test_schema'));
+  }
+
+  /**
+   * Deprecation testing for drupal installed schema version functions.
+   *
+   * @see drupal_get_installed_schema_version()
+   * @see drupal_set_installed_schema_version()
+   */
+  public function testDrupalGetInstalledSchemaVersion() {
+    $this->expectDeprecation('drupal_get_installed_schema_version() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use \Drupal\Core\Update\UpdateHookRegistry::getInstalledVersion() or \Drupal\Core\Update\UpdateHookRegistry::getAllInstalledVersions() instead. See https://www.drupal.org/node/2444417');
+    $this->assertIsArray(drupal_get_installed_schema_version(NULL, TRUE, TRUE));
+    $this->expectDeprecation('drupal_set_installed_schema_version() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. Use \Drupal\Core\Update\UpdateHookRegistry::setInstalledVersion() instead. See https://www.drupal.org/node/2444417');
+    drupal_set_installed_schema_version('system', 8001);
+  }
+
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/UpdateDescriptionTest.php b/core/tests/Drupal/KernelTests/Core/Extension/UpdateDescriptionTest.php
index 371a5f7f03bb..0c12d3ac197c 100644
--- a/core/tests/Drupal/KernelTests/Core/Extension/UpdateDescriptionTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Extension/UpdateDescriptionTest.php
@@ -23,7 +23,7 @@ class UpdateDescriptionTest extends KernelTestBase {
    */
   public function testUpdateGetUpdateList() {
     require_once $this->root . '/core/includes/update.inc';
-    drupal_set_installed_schema_version('update_test_description', 8000);
+    \Drupal::service('update.update_hook_registry')->setInstalledVersion('update_test_description', 8000);
     \Drupal::moduleHandler()->loadInclude('update_test_description', 'install');
 
     $updates = update_get_update_list();
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/UpdateSchemaTest.php b/core/tests/Drupal/KernelTests/Core/Extension/UpdateSchemaTest.php
index 607935a92d7c..bcdca37c0ba0 100644
--- a/core/tests/Drupal/KernelTests/Core/Extension/UpdateSchemaTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Extension/UpdateSchemaTest.php
@@ -19,12 +19,12 @@ class UpdateSchemaTest extends KernelTestBase {
   /**
    * Tests the function parses schema updates as integer numbers.
    *
-   * @see drupal_get_schema_versions()
+   * @see \Drupal\Core\Update\UpdateHookRegistry::getAvailableUpdates()
    */
   public function testDrupalGetSchemaVersionsInt() {
     \Drupal::state()->set('update_test_schema_version', 8001);
     $this->installSchema('update_test_schema', ['update_test_schema_table']);
-    $schema = drupal_get_schema_versions('update_test_schema');
+    $schema = \Drupal::service('update.update_hook_registry')->getAvailableUpdates('update_test_schema');
     foreach ($schema as $version) {
       $this->assertIsInt($version);
     }
diff --git a/core/tests/Drupal/Tests/Core/Update/UpdateHookRegistryTest.php b/core/tests/Drupal/Tests/Core/Update/UpdateHookRegistryTest.php
new file mode 100644
index 000000000000..a220a0ecb053
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Update/UpdateHookRegistryTest.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Drupal\Tests\Core\Update;
+
+use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
+use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
+use Drupal\Core\Update\UpdateHookRegistry;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Simulates a hook_update_N function.
+ */
+function under_test_update_3000() {
+
+}
+
+/**
+ * Simulates a hook_update_N function.
+ *
+ * When filtered this will be rejected.
+ */
+function bad_3() {
+
+}
+
+/**
+ * Simulates a hook_update_N function.
+ */
+function under_test_update_1() {
+
+}
+
+/**
+ * Simulates a hook_update_N functions.
+ *
+ * When filtered this will be rejected.
+ */
+function failed_22_update() {
+
+}
+
+/**
+ * Simulates a hook_update_N function.
+ */
+function under_test_update_20() {
+
+}
+
+/**
+ * Simulates a hook_update_N function.
+ *
+ * When filtered this will be rejected.
+ */
+function under_test_update_1234_failed() {
+
+}
+
+/**
+ * @coversDefaultClass \Drupal\Core\Update\UpdateHookRegistry
+ * @group Update
+ */
+class UpdateHookRegistryTest extends UnitTestCase {
+
+  /**
+   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $keyValueStore;
+
+  /**
+   * @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $keyValueFactory;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->keyValueFactory = $this->createMock(KeyValueFactoryInterface::class);
+    $this->keyValueStore = $this->createMock(KeyValueStoreInterface::class);
+
+    $this->keyValueFactory
+      ->method('get')
+      ->with('system.schema')
+      ->willReturn($this->keyValueStore);
+  }
+
+  /**
+   * @covers ::getAvailableUpdates
+   */
+  public function testGetVersions() {
+    $module_name = 'drupal\tests\core\update\under_test';
+
+    $update_registry = new UpdateHookRegistry([], $this->keyValueStore);
+
+    // Only under_test_update_X - passes through the filter.
+    $expected = [1, 20, 3000];
+    $actual = $update_registry->getAvailableUpdates($module_name);
+
+    $this->assertSame($expected, $actual);
+  }
+
+  /**
+   * @covers ::getInstalledVersion
+   * @covers ::getAllInstalledVersions
+   * @covers ::setInstalledVersion
+   * @covers ::deleteInstalledVersion
+   */
+  public function testGetInstalledVersion() {
+    $versions = [
+      'module1' => 1,
+      'module2' => 20,
+      'module3' => 3000,
+    ];
+
+    $this->keyValueStore
+      ->method('getAll')
+      ->willReturnCallback(static function () use (&$versions) {
+        return $versions;
+      });
+    $this->keyValueStore
+      ->method('get')
+      ->willReturnCallback(static function ($key) use (&$versions) {
+        return $versions[$key];
+      });
+    $this->keyValueStore
+      ->method('delete')
+      ->willReturnCallback(static function ($key) use (&$versions) {
+        $versions[$key] = UpdateHookRegistry::SCHEMA_UNINSTALLED;
+      });
+    $this->keyValueStore
+      ->method('set')
+      ->willReturnCallback(static function ($key, $value) use (&$versions) {
+        $versions[$key] = $value;
+      });
+
+    $update_registry = new UpdateHookRegistry([], $this->keyValueStore);
+
+    $this->assertSame(3000, $update_registry->getInstalledVersion('module3'));
+    $update_registry->setInstalledVersion('module3', 3001);
+    $this->assertSame(3001, $update_registry->getInstalledVersion('module3'));
+    $this->assertSame($versions, $update_registry->getAllInstalledVersions());
+    $update_registry->deleteInstalledVersion('module3');
+    $this->assertSame(UpdateHookRegistry::SCHEMA_UNINSTALLED, $update_registry->getInstalledVersion('module3'));
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/UpdatePathTestTrait.php b/core/tests/Drupal/Tests/UpdatePathTestTrait.php
index d3824943f622..3978f7e37c41 100644
--- a/core/tests/Drupal/Tests/UpdatePathTestTrait.php
+++ b/core/tests/Drupal/Tests/UpdatePathTestTrait.php
@@ -58,9 +58,7 @@ protected function runUpdates($update_url = NULL) {
         $this->fail('The update failed with the following message: "' . reset($failure)->getText() . '"');
       }
 
-      // Ensure that there are no pending updates. Clear the schema version
-      // static cache first in case it was accessed before running updates.
-      drupal_get_installed_schema_version(NULL, TRUE);
+      // Ensure that there are no pending updates.
       foreach (['update', 'post_update'] as $update_type) {
         switch ($update_type) {
           case 'update':
@@ -162,11 +160,7 @@ protected function doSelectionTest() {
   protected function ensureUpdatesToRun() {
     \Drupal::service('module_installer')->install(['update_script_test']);
     // Reset the schema so there is an update to run.
-    \Drupal::database()->update('key_value')
-      ->fields(['value' => serialize(8000)])
-      ->condition('collection', 'system.schema')
-      ->condition('name', 'update_script_test')
-      ->execute();
+    \Drupal::service('update.update_hook_registry')->setInstalledVersion('update_script_test', 8000);
   }
 
 }
-- 
GitLab