diff --git a/core/core.services.yml b/core/core.services.yml
index 2d044d1..def1403 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1519,6 +1519,12 @@ services:
       - { name: placeholder_strategy, priority: -1000 }
   email.validator:
     class: Egulias\EmailValidator\EmailValidator
+  update.post_update_registry:
+    class: Drupal\Core\Update\UpdateRegistry
+    factory: ['@update.post_update_registry_factory', create]
+  update.post_update_registry_factory:
+    class: Drupal\Core\Update\UpdateRegistryFactory
+    parent: container.trait
 
   response_filter.active_link:
     class: Drupal\Core\EventSubscriber\ActiveLinkResponseFilter
diff --git a/core/includes/update.inc b/core/includes/update.inc
index cd1e2ae..683e7fe 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -182,6 +182,7 @@ function update_do_one($module, $number, $dependency_map, &$context) {
     // @TODO We may want to do different error handling for different
     // exception types, but for now we'll just log the exception and
     // return the message for printing.
+    // @see https://www.drupal.org/node/2564311
     catch (Exception $e) {
       watchdog_exception('update', $e);
 
@@ -214,7 +215,66 @@ function update_do_one($module, $number, $dependency_map, &$context) {
     drupal_set_installed_schema_version($module, $number);
   }
 
-  $context['message'] = 'Updating ' . Html::escape($module) . ' module';
+  $context['message'] = t('Updating @module', ['@module' => $module]);
+}
+
+/**
+ * Executes a single hook_post_update_NAME().
+ *
+ * @param string $function
+ *   The function name, that should be executed.
+ * @param array $context
+ *   The batch context array.
+ */
+function update_invoke_post_update($function, &$context) {
+  $ret = [];
+
+  // If this update was aborted in a previous step, or has a dependency that was
+  // aborted in a previous step, go no further.
+  if (!empty($context['results']['#abort'])) {
+    return;
+  }
+
+  list($module, $name) = explode('_post_update_', $function, 2);
+  module_load_include('php', $module, $module . '.post_update');
+  if (function_exists($function)) {
+    try {
+      $ret['results']['query'] = $function($context['sandbox']);
+      $ret['results']['success'] = TRUE;
+
+      \Drupal::service('update.post_update_registry')->registerInvokedUpdates([$function]);
+    }
+    // @TODO We may want to do different error handling for different exception
+    // types, but for now we'll just log the exception and return the message
+    // for printing.
+    // @see https://www.drupal.org/node/2564311
+    catch (Exception $e) {
+      watchdog_exception('update', $e);
+
+      $variables = Error::decodeException($e);
+      unset($variables['backtrace']);
+      $ret['#abort'] = [
+        'success' => FALSE,
+        'query' => t('%type: @message in %function (line %line of %file).', $variables),
+      ];
+    }
+  }
+
+  if (isset($context['sandbox']['#finished'])) {
+    $context['finished'] = $context['sandbox']['#finished'];
+    unset($context['sandbox']['#finished']);
+  }
+  if (!isset($context['results'][$module][$name])) {
+    $context['results'][$module][$name] = array();
+  }
+  $context['results'][$module][$name] = array_merge($context['results'][$module][$name], $ret);
+
+  if (!empty($ret['#abort'])) {
+    // Record this function in the list of updates that were aborted.
+    $context['results']['#abort'][] = $function;
+  }
+
+  $context['message'] = t('Post updating @module', ['@module' => $module]);
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php
index 01416ba..4863121 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -311,7 +311,7 @@ public function access($operation, AccountInterface $account = NULL, $return_as_
         ->getAccessControlHandler($this->entityTypeId)
         ->createAccess($this->bundle(), $account, [], $return_as_object);
     }
-    return $this->entityManager()
+    return  $this->entityManager()
       ->getAccessControlHandler($this->entityTypeId)
       ->access($this, $operation, LanguageInterface::LANGCODE_DEFAULT, $account, $return_as_object);
   }
diff --git a/core/lib/Drupal/Core/Entity/entity.api.php b/core/lib/Drupal/Core/Entity/entity.api.php
index 6bdde07..c2638c4 100644
--- a/core/lib/Drupal/Core/Entity/entity.api.php
+++ b/core/lib/Drupal/Core/Entity/entity.api.php
@@ -517,19 +517,19 @@
  * @param string $operation
  *   The operation that is to be performed on $entity.
  * @param \Drupal\Core\Session\AccountInterface $account
- *   The account trying to access the entity.
+ *    The account trying to access the entity.
  * @param string $langcode
- *   The code of the language $entity is accessed in.
+ *    The code of the language $entity is accessed in.
  *
  * @return \Drupal\Core\Access\AccessResultInterface
- *   The access result. The final result is calculated by using
- *   \Drupal\Core\Access\AccessResultInterface::orIf() on the result of every
- *   hook_entity_access() and hook_ENTITY_TYPE_access() implementation, and the
- *   result of the entity-specific checkAccess() method in the entity access
- *   control handler. Be careful when writing generalized access checks shared
- *   between routing and entity checks: routing uses the andIf() operator. So
- *   returning an isNeutral() does not determine entity access at all but it
- *   always ends up denying access while routing.
+ *    The access result. The final result is calculated by using
+ *    \Drupal\Core\Access\AccessResultInterface::orIf() on the result of every
+ *    hook_entity_access() and hook_ENTITY_TYPE_access() implementation, and the
+ *    result of the entity-specific checkAccess() method in the entity access
+ *    control handler. Be careful when writing generalized access checks shared
+ *    between routing and entity checks: routing uses the andIf() operator. So
+ *    returning an isNeutral() does not determine entity access at all but it
+ *    always ends up denying access while routing.
  *
  * @see \Drupal\Core\Entity\EntityAccessControlHandler
  * @see hook_entity_create_access()
@@ -550,12 +550,12 @@ function hook_entity_access(\Drupal\Core\Entity\EntityInterface $entity, $operat
  * @param string $operation
  *   The operation that is to be performed on $entity.
  * @param \Drupal\Core\Session\AccountInterface $account
- *   The account trying to access the entity.
+ *    The account trying to access the entity.
  * @param string $langcode
- *   The code of the language $entity is accessed in.
+ *    The code of the language $entity is accessed in.
  *
  * @return \Drupal\Core\Access\AccessResultInterface
- *   The access result. hook_entity_access() has detailed documentation.
+ *    The access result. hook_entity_access() has detailed documentation.
  *
  * @see \Drupal\Core\Entity\EntityAccessControlHandler
  * @see hook_ENTITY_TYPE_create_access()
@@ -572,16 +572,16 @@ function hook_ENTITY_TYPE_access(\Drupal\Core\Entity\EntityInterface $entity, $o
  * Control entity create access.
  *
  * @param \Drupal\Core\Session\AccountInterface $account
- *   The account trying to access the entity.
+ *    The account trying to access the entity.
  * @param array $context
- *   An associative array of additional context values. By default it contains
- *   language:
- *   - langcode - the current language code.
+ *    An associative array of additional context values. By default it contains
+ *    language:
+ *    - langcode - the current language code.
  * @param string $entity_bundle
- *   The entity bundle name.
+ *    The entity bundle name.
  *
  * @return \Drupal\Core\Access\AccessResultInterface
- *   The access result.
+ *    The access result.
  *
  * @see \Drupal\Core\Entity\EntityAccessControlHandler
  * @see hook_entity_access()
@@ -598,16 +598,16 @@ function hook_entity_create_access(\Drupal\Core\Session\AccountInterface $accoun
  * Control entity create access for a specific entity type.
  *
  * @param \Drupal\Core\Session\AccountInterface $account
- *   The account trying to access the entity.
+ *    The account trying to access the entity.
  * @param array $context
- *   An associative array of additional context values. By default it contains
- *   language:
- *   - langcode - the current language code.
+ *    An associative array of additional context values. By default it contains
+ *    language:
+ *    - langcode - the current language code.
  * @param string $entity_bundle
- *   The entity bundle name.
+ *    The entity bundle name.
  *
  * @return \Drupal\Core\Access\AccessResultInterface
- *   The access result.
+ *    The access result.
  *
  * @see \Drupal\Core\Entity\EntityAccessControlHandler
  * @see hook_ENTITY_TYPE_access()
@@ -1064,7 +1064,7 @@ function hook_ENTITY_TYPE_translation_insert(\Drupal\Core\Entity\EntityInterface
  *
  * This hook runs once the entity translation has been deleted from storage.
  *
- * @param \Drupal\Core\Entity\EntityInterface $translation
+ * @param \Drupal\Core\Entity\EntityInterface $entity
  *   The original entity object.
  *
  * @ingroup entity_crud
@@ -1084,7 +1084,7 @@ function hook_entity_translation_delete(\Drupal\Core\Entity\EntityInterface $tra
  *
  * This hook runs once the entity translation has been deleted from storage.
  *
- * @param \Drupal\Core\Entity\EntityInterface $translation
+ * @param \Drupal\Core\Entity\EntityInterface $entity
  *   The original entity object.
  *
  * @ingroup entity_crud
@@ -1679,7 +1679,7 @@ function hook_entity_base_field_info(\Drupal\Core\Entity\EntityTypeInterface $en
  * @see hook_entity_bundle_field_info_alter()
  *
  * @todo WARNING: This hook will be changed in
- * https://www.drupal.org/node/2346329.
+ *   https://www.drupal.org/node/2346329.
  */
 function hook_entity_base_field_info_alter(&$fields, \Drupal\Core\Entity\EntityTypeInterface $entity_type) {
   // Alter the mymodule_text field to use a custom class.
@@ -1714,7 +1714,7 @@ function hook_entity_base_field_info_alter(&$fields, \Drupal\Core\Entity\EntityT
  * @see \Drupal\Core\Entity\EntityManagerInterface::getFieldDefinitions()
  *
  * @todo WARNING: This hook will be changed in
- * https://www.drupal.org/node/2346347.
+ *   https://www.drupal.org/node/2346347.
  */
 function hook_entity_bundle_field_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
   // Add a property only to nodes of the 'article' bundle.
@@ -1743,7 +1743,7 @@ function hook_entity_bundle_field_info(\Drupal\Core\Entity\EntityTypeInterface $
  * @see hook_entity_bundle_field_info()
  *
  * @todo WARNING: This hook will be changed in
- * https://www.drupal.org/node/2346347.
+ *   https://www.drupal.org/node/2346347.
  */
 function hook_entity_bundle_field_info_alter(&$fields, \Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle) {
   if ($entity_type->id() == 'node' && $bundle == 'article' && !empty($fields['mymodule_text'])) {
diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
index 3238ce6..c088eed 100644
--- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
+++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
@@ -91,14 +91,29 @@ class ExtensionDiscovery {
   protected $fileCache;
 
   /**
+   * The site path.
+   *
+   * @var string
+   */
+  protected $sitePath;
+
+  /**
    * Constructs a new ExtensionDiscovery object.
    *
    * @param string $root
    *   The app root.
+   * @param bool $use_file_cache
+   *   Whether file cache should be used.
+   * @param string[] $profile_directories
+   *   The available profile directories
+   * @param string $site_path
+   *   The path to the site.
    */
-  public function __construct($root) {
+  public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL) {
     $this->root = $root;
-    $this->fileCache = FileCacheFactory::get('extension_discovery');
+    $this->fileCache = $use_file_cache ? FileCacheFactory::get('extension_discovery') : NULL;
+    $this->profileDirectories = $profile_directories;
+    $this->sitePath = $site_path;
   }
 
   /**
@@ -172,7 +187,7 @@ public function scan($type, $include_tests = NULL) {
       $searchdirs[static::ORIGIN_SITE] = \Drupal::service('site.path');
     }
     else {
-      $searchdirs[static::ORIGIN_SITE] = DrupalKernel::findSitePath(Request::createFromGlobals());
+      $searchdirs[static::ORIGIN_SITE] = $this->sitePath ?: DrupalKernel::findSitePath(Request::createFromGlobals());
     }
 
     // Unless an explicit value has been passed, manually check whether we are
@@ -180,7 +195,7 @@ public function scan($type, $include_tests = NULL) {
     // Test extensions can also be included for debugging purposes by setting a
     // variable in settings.php.
     if (!isset($include_tests)) {
-      $include_tests = drupal_valid_test_ua() || Settings::get('extension_discovery_scan_tests');
+      $include_tests = Settings::get('extension_discovery_scan_tests') || drupal_valid_test_ua();
     }
 
     $files = array();
@@ -427,7 +442,7 @@ protected function scanDirectory($dir, $include_tests) {
         continue;
       }
 
-      if ($cached_extension = $this->fileCache->get($fileinfo->getPathName())) {
+      if ($this->fileCache && $cached_extension = $this->fileCache->get($fileinfo->getPathName())) {
         $files[$cached_extension->getType()][$key] = $cached_extension;
         continue;
       }
@@ -467,7 +482,10 @@ protected function scanDirectory($dir, $include_tests) {
       $extension->origin = $dir;
 
       $files[$type][$key] = $extension;
-      $this->fileCache->set($fileinfo->getPathName(), $extension);
+
+      if ($this->fileCache) {
+        $this->fileCache->set($fileinfo->getPathName(), $extension);
+      }
     }
     return $files;
   }
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index a8124d5..92bdb80 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -260,6 +260,11 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
         }
         drupal_set_installed_schema_version($module, $version);
 
+        // Ensure that all post_update functions are registered already.
+        /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
+        $post_update_registry = \Drupal::service('update.post_update_registry');
+        $post_update_registry->registerInvokedUpdates($post_update_registry->getModuleUpdateFunctions($module));
+
         // Record the fact that it was installed.
         $modules_installed[] = $module;
 
@@ -445,6 +450,10 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
 
       $schema_store = \Drupal::keyValue('system.schema');
       $schema_store->delete($module);
+
+      /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
+      $post_update_registry = \Drupal::service('update.post_update_registry');
+      $post_update_registry->filterOutInvokedUpdatesByModule($module);
     }
     \Drupal::service('router.builder')->setRebuildNeeded();
     drupal_get_installed_schema_version(NULL, TRUE);
diff --git a/core/lib/Drupal/Core/Extension/module.api.php b/core/lib/Drupal/Core/Extension/module.api.php
index d7d5332..ce72ded 100644
--- a/core/lib/Drupal/Core/Extension/module.api.php
+++ b/core/lib/Drupal/Core/Extension/module.api.php
@@ -660,6 +660,47 @@ function hook_update_N(&$sandbox) {
 }
 
 /**
+ * Executes an update which is intended to update data, like entities.
+ *
+ * These implementations have to be placed in a MODULE.post_update.php file.
+ *
+ * These updates are executed after all hook_update_N() implementations. At this
+ * stage Drupal is already fully repaired so you can use any API as you wish.
+ *
+ * NAME can be arbitrary machine names. In contrast to hook_update_N() the order
+ * of functions in the file is the only thing which ensures the execution order
+ * of those functions.
+ *
+ * Drupal also ensures to not execute the same hook_post_update_NAME() function
+ * twice.
+ *
+ * @param array $sandbox
+ *   Stores information for batch updates. See above for more information.
+ *
+ * @throws \Drupal\Core\Utility\UpdateException|PDOException
+ *   In case of error, update hooks should throw an instance of
+ *   \Drupal\Core\Utility\UpdateException with a meaningful message for the
+ *   user. If a database query fails for whatever reason, it will throw a
+ *   PDOException.
+ *
+ * @return string|null
+ *   Optionally, hook_post_update_NAME() hooks may return a translated string
+ *   that will be displayed to the user after the update has completed. If no
+ *   message is returned, no message will be presented to the user.
+ *
+ * @ingroup update_api
+ *
+ * @see hook_update_N()
+ */
+function hook_post_update_NAME(&$sandbox) {
+  $node = \Drupal\node\Entity\Node::load(123);
+  $node->setTitle('foo');
+  $node->save();
+
+  return t('Node %nid saved', ['%nid' => $node->id()]);
+}
+
+/**
  * Return an array of information about module update dependencies.
  *
  * This can be used to indicate update functions from other modules that your
diff --git a/core/lib/Drupal/Core/Update/UpdateRegistry.php b/core/lib/Drupal/Core/Update/UpdateRegistry.php
new file mode 100644
index 0000000..0190e04
--- /dev/null
+++ b/core/lib/Drupal/Core/Update/UpdateRegistry.php
@@ -0,0 +1,267 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Update\UpdateRegistry.
+ */
+
+namespace Drupal\Core\Update;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Extension\ExtensionDiscovery;
+use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
+
+/**
+ * Provides all and missing update implementations.
+ *
+ * Note: This registry is specific to a type of updates, like 'post_update' as
+ * example.
+ *
+ * It therefore scans for functions named like the type of updates, so it looks
+ * like MODULE_UPDATETYPE_NAME() with NAME being a machine name.
+ */
+class UpdateRegistry {
+
+  /**
+   * The used update name.
+   *
+   * @var string
+   */
+  protected $updateType = 'post_update';
+
+  /**
+   * The app root.
+   *
+   * @var string
+   */
+  protected $root;
+
+  /**
+   * The filename of the log file.
+   *
+   * @var string
+   */
+  protected $logFilename;
+
+  /**
+   * @var string[]
+   */
+  protected $enabledModules;
+
+  /**
+   * The key value storage.
+   *
+   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+   */
+  protected $keyValue;
+
+  /**
+   * Should we respect update functions in tests.
+   *
+   * @var bool|null
+   */
+  protected $includeTests = NULL;
+
+  /**
+   * The site path.
+   *
+   * @var string
+   */
+  protected $sitePath;
+
+  /**
+   * Constructs a new UpdateRegistry.
+   *
+   * @param string $root
+   *   The app root.
+   * @param string $site_path
+   *   The site path.
+   * @param string[] $enabled_modules
+   *   A list of enabled modules.
+   * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value
+   *   The key value store.
+   * @param bool|NULL $include_tests
+   *   (optional) A flag whether to include tests in the scanning of modules.
+   */
+  public function __construct($root, $site_path, array $enabled_modules, KeyValueStoreInterface $key_value, $include_tests = NULL) {
+    $this->root = $root;
+    $this->sitePath = $site_path;
+    $this->enabledModules = $enabled_modules;
+    $this->keyValue = $key_value;
+    $this->includeTests = $include_tests;
+  }
+
+  /**
+   * Gets all available update functions.
+   *
+   * @return callable[]
+   *   A list of update functions.
+   */
+  protected function getAvailableUpdateFunctions() {
+    $regexp = '/^(?<module>.+)_' . $this->updateType . '_(?<name>.+)$/';
+    $functions = get_defined_functions();
+
+    $updates = [];
+    foreach (preg_grep('/_' . $this->updateType . '_/', $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)) {
+        if (in_array($matches['module'], $this->enabledModules)) {
+          $updates[] = $matches['module'] . '_' . $this->updateType . '_' . $matches['name'];
+        }
+      }
+    }
+
+    return $updates;
+  }
+
+  /**
+   * Find all update functions that haven't been executed.
+   *
+   * @return callable[]
+   *   A list of update functions.
+   */
+  public function getPendingUpdateFunctions() {
+    // We need a) the list of active modules (we get that from the config
+    // bootstrap factory) and b) the path to the modules, we use the extension
+    // discovery for that.
+
+    $this->scanExtensionsAndLoadUpdateFiles();
+
+    // First figure out which hook_{$this->updateType}_NAME got executed
+    // already.
+    $existing_update_functions = $this->keyValue->get('existing_updates', []);
+
+    $available_update_functions = $this->getAvailableUpdateFunctions();
+    $not_executed_update_functions = array_diff($available_update_functions, $existing_update_functions);
+
+    return $not_executed_update_functions;
+  }
+
+  /**
+   * Loads all update files for a given list of extension.
+   *
+   * @param \Drupal\Core\Extension\Extension[] $module_extensions
+   *   The extensions used for loading.
+   */
+  protected function loadUpdateFiles(array $module_extensions) {
+    // Load all the {$this->updateType}.php files.
+    foreach ($this->enabledModules as $module) {
+      if (isset($module_extensions[$module])) {
+        $this->loadUpdateFile($module_extensions[$module]);
+      }
+    }
+  }
+
+  /**
+   * Loads the {$this->updateType}.php file for a given extension.
+   *
+   * @param \Drupal\Core\Extension\Extension $module
+   *   The extension of the module to load its file.
+   */
+  protected function loadUpdateFile(Extension $module) {
+    $filename = $this->root . '/' . $module->getPath() . '/' . $module->getName() . ".{$this->updateType}.php";
+    if (file_exists($filename)) {
+      include_once $filename;
+    }
+  }
+
+  /**
+   * Returns a list of all the pending updates.
+   *
+   * @return array[]
+   *   An associative array keyed by module name which contains all information
+   *   about database updates that need to be run, and any updates that are not
+   *   going to proceed due to missing requirements.
+   *
+   *   The subarray for each module can contain the following keys:
+   *   - start: The starting update that is to be processed. If this does not
+   *       exist then do not process any updates for this module as there are
+   *       other requirements that need to be resolved.
+   *   - pending: An array of all the pending updates for the module including
+   *       the description from source code comment for each update function.
+   *       This array is keyed by the update name.
+   */
+  public function getPendingUpdateInformation() {
+    $functions = $this->getPendingUpdateFunctions();
+
+    $ret = [];
+    foreach ($functions as $function) {
+      list($module, $update) = explode("_{$this->updateType}_", $function);
+      // The description for an update comes from its Doxygen.
+      $func = new \ReflectionFunction($function);
+      $description = trim(str_replace(array("\n", '*', '/'), '', $func->getDocComment()), ' ');
+      $ret[$module]['pending'][$update] = $description;
+      if (!isset($ret[$module]['start'])) {
+        $ret[$module]['start'] = $update;
+      }
+    }
+    return $ret;
+  }
+
+  /**
+   * Registers that update fucntions got executed.
+   *
+   * @param string[] $function_names
+   *   The executed update functions.
+   *
+   * @return $this
+   */
+  public function registerInvokedUpdates(array $function_names) {
+    $executed_updates = $this->keyValue->get('existing_updates', []);
+    $executed_updates = array_merge($executed_updates, $function_names);
+    $this->keyValue->set('existing_updates', $executed_updates);
+
+    return $this;
+  }
+
+  /**
+   * Returns all available updates for a given module.
+   *
+   * @param string $module_name
+   *   The module name.
+   *
+   * @return callable[]
+   *   A list of update functions.
+   */
+  public function getModuleUpdateFunctions($module_name) {
+    $this->scanExtensionsAndLoadUpdateFiles();
+    $all_functions = $this->getAvailableUpdateFunctions();
+
+    return array_filter($all_functions, function($function_name) use ($module_name) {
+      list($function_module_name, ) = explode("_{$this->updateType}_", $function_name);
+      return $function_module_name === $module_name;
+    });
+  }
+
+  /**
+   * Scans all module + profile extensions and load the update files.
+   */
+  protected function scanExtensionsAndLoadUpdateFiles() {
+    // Scan the module list.
+    $extension_discovery = new ExtensionDiscovery($this->root, FALSE, [], $this->sitePath);
+    $module_extensions = $extension_discovery->scan('module');
+
+    $profile_extensions = $extension_discovery->scan('profile');
+    $extensions = array_merge($module_extensions, $profile_extensions);
+
+    $this->loadUpdateFiles($extensions);
+  }
+
+  /**
+   * Filters out already executed update functions by module.
+   *
+   * @param string $module
+   *   The module name.
+   */
+  public function filterOutInvokedUpdatesByModule($module) {
+    $existing_update_functions = $this->keyValue->get('existing_updates', []);
+
+    $remaining_update_functions = array_filter($existing_update_functions, function($function_name) use ($module) {
+      return strpos($function_name, "{$module}_{$this->updateType}_") !== 0;
+    });
+
+    $this->keyValue->set('existing_updates', array_values($remaining_update_functions));
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Update/UpdateRegistryFactory.php b/core/lib/Drupal/Core/Update/UpdateRegistryFactory.php
new file mode 100644
index 0000000..ef41154
--- /dev/null
+++ b/core/lib/Drupal/Core/Update/UpdateRegistryFactory.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Update\UpdateRegistryFactory.
+ */
+
+namespace Drupal\Core\Update;
+
+use Symfony\Component\DependencyInjection\ContainerAwareInterface;
+use Symfony\Component\DependencyInjection\ContainerAwareTrait;
+
+/**
+ * Service factory for the update registry.
+ */
+class UpdateRegistryFactory implements ContainerAwareInterface {
+
+  use ContainerAwareTrait;
+
+  /**
+   * Creates a new UpdateRegistry instance.
+   *
+   * @return \Drupal\Core\Update\UpdateRegistry
+   *   The update registry instance.
+   */
+  public function create() {
+    return new UpdateRegistry($this->container->get('app.root'), $this->container->get('site.path'), array_keys($this->container->get('module_handler')->getModuleList()), $this->container->get('keyvalue')->get('post_update'));
+  }
+
+}
diff --git a/core/modules/block/block.install b/core/modules/block/block.install
index 99ce4ef..9abe82e 100644
--- a/core/modules/block/block.install
+++ b/core/modules/block/block.install
@@ -45,8 +45,9 @@ function block_update_8001() {
   }
 
   // Contributed modules should leverage hook_update_dependencies() in order to
-  // be executed before block_update_8002(), so they can update their context
-  // mappings, if wanted.
+  // be executed after block_update_8001(). The blocks are then disabled if the
+  // contexts are still missing via
+  // block_post_update_disable_blocks_with_missing_contexts().
   $config_factory = \Drupal::configFactory();
   $backup_values = $update_backup = [];
 
@@ -95,50 +96,10 @@ function block_update_8001() {
 }
 
 /**
- * Disable all blocks with missing context IDs in block_update_8001().
+ * Placeholder for the previous 8002 update.
  */
 function block_update_8002() {
-  $block_update_8001 = \Drupal::keyValue('update_backup')->get('block_update_8001', []);
-
-  $block_ids = array_keys($block_update_8001);
-  $config_factory = \Drupal::configFactory();
-  /** @var \Drupal\Core\Config\Config[] $blocks */
-  $blocks = [];
-  foreach ($block_ids as $block_id) {
-    $blocks[$block_id] = $block = $config_factory->getEditable('block.block.' . $block_id);
-    // This block will have an invalid context mapping service and must be
-    // disabled in order to prevent information disclosure.
-
-    // Disable currently enabled blocks.
-    if ($block_update_8001[$block_id]['status']) {
-      $block->set('status', FALSE);
-      $block->save(TRUE);
-    }
-  }
-
-  // Provides a list of plugin labels, keyed by plugin ID.
-  $condition_plugin_id_label_map = array_column(\Drupal::service('plugin.manager.condition')->getDefinitions(), 'label', 'id');
-
-  // Override with the UI labels we are aware of. Sadly they are not machine
-  // accessible, see
-  // \Drupal\node\Plugin\Condition\NodeType::buildConfigurationForm().
-  $condition_plugin_id_label_map['node_type'] = t('Content types');
-  $condition_plugin_id_label_map['request_path'] = t('Pages');
-  $condition_plugin_id_label_map['user_role'] = t('Roles');
-
-  if (count($block_ids) > 0) {
-    $message = t('Encountered an unknown context mapping key coming probably from a contributed or custom module: One or more mappings could not be updated. Please manually review your visibility settings for the following blocks, which are disabled now:');
-    $message .= '<ul>';
-    foreach ($blocks as $disabled_block_id => $disabled_block) {
-      $message .= '<li>' . t('@label (Visibility: @plugin_ids)', array(
-            '@label' => $disabled_block->get('settings.label'),
-            '@plugin_ids' => implode(', ', array_intersect_key($condition_plugin_id_label_map, array_flip(array_keys($block_update_8001[$disabled_block_id]['missing_context_ids']))))
-          )) . '</li>';
-    }
-    $message .= '</ul>';
-
-    return $message;
-  }
+  \Drupal::state()->set('block_update_8002_placeholder', TRUE);
 }
 
 /**
diff --git a/core/modules/block/block.post_update.php b/core/modules/block/block.post_update.php
new file mode 100644
index 0000000..17e0497
--- /dev/null
+++ b/core/modules/block/block.post_update.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @file
+ * Post update functions for Block.
+ */
+
+/**
+ * @addtogroup updates-8.0.0-beta
+ * @{
+ */
+
+/**
+ * Disable all blocks with missing context IDs in block_update_8001().
+ */
+function block_post_update_disable_blocks_with_missing_contexts() {
+  // Don't execute the function if block_update_8002() got executed already,
+  // which used to do the same. Note: Its okay to check here, because
+  // update_do_one() does not update the installed schema version until the
+  // batch is finished.
+  $module_schema = drupal_get_installed_schema_version('block');
+
+  // The state entry 'block_update_8002_placeholder' is used in order to
+  // indicate that the placeholder block_update_8002() function has been
+  // executed, so this function needs to be executed as well. If the non
+  // placeholder version of block_update_8002() got executed already, the state
+  // won't be set and we skip this update.
+  if ($module_schema >= 8002 && !\Drupal::state()->get('block_update_8002_placeholder', FALSE)) {
+    return;
+  }
+
+  // Cleanup the state entry as its no longer needed.
+  \Drupal::state()->delete('block_update_8002');
+
+  $block_update_8001 = \Drupal::keyValue('update_backup')->get('block_update_8001', []);
+
+  $block_ids = array_keys($block_update_8001);
+  $block_storage = \Drupal::entityManager()->getStorage('block');
+  $blocks = $block_storage->loadMultiple($block_ids);
+  /** @var $blocks \Drupal\block\BlockInterface[] */
+  foreach ($blocks as $block) {
+    // This block will have an invalid context mapping service and must be
+    // disabled in order to prevent information disclosure.
+
+    // Disable currently enabled blocks.
+    if ($block_update_8001[$block->id()]['status']) {
+      $block->setStatus(FALSE);
+      $block->save();
+    }
+  }
+
+  // Provides a list of plugin labels, keyed by plugin ID.
+  $condition_plugin_id_label_map = array_column(\Drupal::service('plugin.manager.condition')->getDefinitions(), 'label', 'id');
+
+  // Override with the UI labels we are aware of. Sadly they are not machine
+  // accessible, see
+  // \Drupal\node\Plugin\Condition\NodeType::buildConfigurationForm().
+  $condition_plugin_id_label_map['node_type'] = t('Content types');
+  $condition_plugin_id_label_map['request_path'] = t('Pages');
+  $condition_plugin_id_label_map['user_role'] = t('Roles');
+
+  if (count($block_ids) > 0) {
+    $message = t('Encountered an unknown context mapping key coming probably from a contributed or custom module: One or more mappings could not be updated. Please manually review your visibility settings for the following blocks, which are disabled now:');
+    $message .= '<ul>';
+    foreach ($blocks as $disabled_block_id => $disabled_block) {
+      $message .= '<li>' . t('@label (Visibility: @plugin_ids)', array(
+          '@label' => $disabled_block->get('settings')['label'],
+          '@plugin_ids' => implode(', ', array_intersect_key($condition_plugin_id_label_map, array_flip(array_keys($block_update_8001[$disabled_block_id]['missing_context_ids']))))
+        )) . '</li>';
+    }
+    $message .= '</ul>';
+
+    return $message;
+  }
+}
+
+/**
+ * @} End of "addtogroup updates-8.0.0-beta".
+ */
diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php
index fa8104d..61e89a2 100644
--- a/core/modules/system/src/Controller/DbUpdateController.php
+++ b/core/modules/system/src/Controller/DbUpdateController.php
@@ -15,6 +15,7 @@
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Site\Settings;
 use Drupal\Core\State\StateInterface;
+use Drupal\Core\Update\UpdateRegistry;
 use Drupal\Core\Url;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Response;
@@ -75,6 +76,13 @@ class DbUpdateController extends ControllerBase {
   protected $root;
 
   /**
+   * The post update registry.
+   *
+   * @var \Drupal\Core\Update\UpdateRegistry
+   */
+  protected $postUpdateRegistry;
+
+  /**
    * Constructs a new UpdateController.
    *
    * @param string $root
@@ -91,8 +99,10 @@ class DbUpdateController extends ControllerBase {
    *   The current user.
    * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer
    *   The bare HTML page renderer.
+   * @param \Drupal\Core\Update\UpdateRegistry $post_update_registry
+   *   The post update registry.
    */
-  public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer) {
+  public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer, UpdateRegistry $post_update_registry) {
     $this->root = $root;
     $this->keyValueExpirableFactory = $key_value_expirable_factory;
     $this->cache = $cache;
@@ -100,6 +110,7 @@ public function __construct($root, KeyValueExpirableFactoryInterface $key_value_
     $this->moduleHandler = $module_handler;
     $this->account = $account;
     $this->bareHtmlPageRenderer = $bare_html_page_renderer;
+    $this->postUpdateRegistry = $post_update_registry;
   }
 
   /**
@@ -113,7 +124,8 @@ public static function create(ContainerInterface $container) {
       $container->get('state'),
       $container->get('module_handler'),
       $container->get('current_user'),
-      $container->get('bare_html_page_renderer')
+      $container->get('bare_html_page_renderer'),
+      $container->get('update.post_update_registry')
     );
   }
 
@@ -257,35 +269,52 @@ protected function selection(Request $request) {
     // Ensure system.module's updates appear first.
     $build['start']['system'] = array();
 
-    $updates = update_get_update_list();
     $starting_updates = array();
     $incompatible_updates_exist = FALSE;
-    foreach ($updates as $module => $update) {
-      if (!isset($update['start'])) {
-        $build['start'][$module] = array(
-          '#type' => 'item',
-          '#title' => $module . ' module',
-          '#markup'  => $update['warning'],
-          '#prefix' => '<div class="messages messages--warning">',
-          '#suffix' => '</div>',
-        );
-        $incompatible_updates_exist = TRUE;
-        continue;
-      }
-      if (!empty($update['pending'])) {
-        $starting_updates[$module] = $update['start'];
-        $build['start'][$module] = array(
-          '#type' => 'hidden',
-          '#value' => $update['start'],
-        );
-        $build['start'][$module . '_updates'] = array(
-          '#theme' => 'item_list',
-          '#items' => $update['pending'],
-          '#title' => $module . ' module',
-        );
+    $updates_per_module = [];
+    foreach (['update', 'post_update'] as $update_type) {
+      switch ($update_type) {
+        case 'update':
+          $updates = update_get_update_list();
+          break;
+        case 'post_update':
+          $updates = $this->postUpdateRegistry->getPendingUpdateInformation();
+          break;
       }
-      if (isset($update['pending'])) {
-        $count = $count + count($update['pending']);
+      foreach ($updates as $module => $update) {
+        if (!isset($update['start'])) {
+          $build['start'][$module] = array(
+            '#type' => 'item',
+            '#title' => $module . ' module',
+            '#markup' => $update['warning'],
+            '#prefix' => '<div class="messages messages--warning">',
+            '#suffix' => '</div>',
+          );
+          $incompatible_updates_exist = TRUE;
+          continue;
+        }
+        if (!empty($update['pending'])) {
+          $updates_per_module += [$module => []];
+          $updates_per_module[$module] = array_merge($updates_per_module[$module], $update['pending']);
+          $build['start'][$module] = array(
+            '#type' => 'hidden',
+            '#value' => $update['start'],
+          );
+          // Store the previous items in order to merge normal updates and
+          // post_update functions together.
+          $build['start'][$module] = array(
+            '#theme' => 'item_list',
+            '#items' => $updates_per_module[$module],
+            '#title' => $module . ' module',
+          );
+
+          if ($update_type === 'update') {
+            $starting_updates[$module] = $update['start'];
+          }
+        }
+        if (isset($update['pending'])) {
+          $count = $count + count($update['pending']);
+        }
       }
     }
 
@@ -415,7 +444,7 @@ protected function results(Request $request) {
         if ($module != '#abort') {
           $module_has_message = FALSE;
           $info_messages = array();
-          foreach ($updates as $number => $queries) {
+          foreach ($updates as $name => $queries) {
             $messages = array();
             foreach ($queries as $query) {
               // If there is no message for this update, don't show anything.
@@ -439,10 +468,16 @@ protected function results(Request $request) {
 
             if ($messages) {
               $module_has_message = TRUE;
+              if (is_numeric($name)) {
+                $title = $this->t('Update #@count', ['@count' => $name]);
+              }
+              else {
+                $title = $this->t('Update @name', ['@name' => trim($name, '_')]);
+              }
               $info_messages[] = array(
                 '#theme' => 'item_list',
                 '#items' => $messages,
-                '#title' => $this->t('Update #@count', array('@count' => $number)),
+                '#title' => $title,
               );
             }
           }
@@ -575,6 +610,17 @@ protected function triggerBatch(Request $request) {
       }
     }
 
+    $post_updates = $this->postUpdateRegistry->getPendingUpdateFunctions();
+
+    if ($post_updates) {
+      // Now we rebuild all caches and after that execute the hook_post_update()
+      // functions.
+      $operations[] = ['drupal_flush_all_caches', []];
+      foreach ($post_updates as $function) {
+        $operations[] = ['update_invoke_post_update', [$function]];
+      }
+    }
+
     $batch['operations'] = $operations;
     $batch += array(
       'title' => $this->t('Updating'),
diff --git a/core/modules/system/src/Tests/Menu/MenuAccessTest.php b/core/modules/system/src/Tests/Menu/MenuTranslateTest.php
similarity index 57%
rename from core/modules/system/src/Tests/Menu/MenuAccessTest.php
rename to core/modules/system/src/Tests/Menu/MenuTranslateTest.php
index 8867dcb..3641ab1 100644
--- a/core/modules/system/src/Tests/Menu/MenuAccessTest.php
+++ b/core/modules/system/src/Tests/Menu/MenuTranslateTest.php
@@ -2,7 +2,7 @@
 
 /**
  * @file
- * Contains \Drupal\system\Tests\Menu\MenuAccessTest.
+ * Contains \Drupal\system\Tests\Menu\MenuTranslateTest.
  */
 
 namespace Drupal\system\Tests\Menu;
@@ -11,11 +11,12 @@
 use Drupal\simpletest\WebTestBase;
 
 /**
- * Tests the route access checks on menu links.
+ * Tests the _menu_translate() method.
  *
  * @group Menu
+ * @see _menu_translate().
  */
-class MenuAccessTest extends WebTestBase {
+class MenuTranslateTest extends WebTestBase {
 
   /**
    * Modules to enable.
@@ -34,23 +35,9 @@ protected function setUp() {
   }
 
   /**
-   * Tests menu link for route with access check.
-   *
-   * @see \Drupal\menu_test\Access\AccessCheck::access()
+   * Tests _menu_translate().
    */
-  public function testMenuBlockLinksAccessCheck() {
-    $this->drupalPlaceBlock('system_menu_block:account');
-    // Test that there's link rendered on the route.
-    $this->drupalGet('menu_test_access_check_session');
-    $this->assertLink('Test custom route access check');
-    // Page still accessible but thre should not be menu link.
-    $this->drupalGet('menu_test_access_check_session');
-    $this->assertResponse(200);
-    $this->assertNoLink('Test custom route access check');
-    // Test that page is no more accessible.
-    $this->drupalGet('menu_test_access_check_session');
-    $this->assertResponse(403);
-
+  public function testMenuTranslate() {
     // Check for access to a restricted local task from a default local task.
     $this->drupalGet('foo/asdf');
     $this->assertResponse(200);
diff --git a/core/modules/system/src/Tests/Module/InstallTest.php b/core/modules/system/src/Tests/Module/InstallTest.php
index ad81180..5f1a091 100644
--- a/core/modules/system/src/Tests/Module/InstallTest.php
+++ b/core/modules/system/src/Tests/Module/InstallTest.php
@@ -53,6 +53,21 @@ public function testRequiredModuleSchemaVersions() {
     $this->assertTrue($version > 0, 'System module version is > 0.');
     $version = drupal_get_installed_schema_version('user', TRUE);
     $this->assertTrue($version > 0, 'User module version is > 0.');
+
+    $post_update_key_value = \Drupal::keyValue('post_update');
+    $existing_updates = $post_update_key_value->get('existing_updates', []);
+    $this->assertTrue(in_array('module_test_post_update_test', $existing_updates));
+  }
+
+  /**
+   * Ensures that post update functions are removed on uninstall.
+   */
+  public function testUninstallPostUpdateFunctions() {
+    \Drupal::service('module_installer')->uninstall(['module_test']);
+
+    $post_update_key_value = \Drupal::keyValue('post_update');
+    $existing_updates = $post_update_key_value->get('existing_updates', []);
+    $this->assertFalse(in_array('module_test_post_update_test', $existing_updates));
   }
 
   /**
diff --git a/core/modules/system/src/Tests/Module/InstallUninstallTest.php b/core/modules/system/src/Tests/Module/InstallUninstallTest.php
index 2295b75..24549b5 100644
--- a/core/modules/system/src/Tests/Module/InstallUninstallTest.php
+++ b/core/modules/system/src/Tests/Module/InstallUninstallTest.php
@@ -16,7 +16,10 @@
  */
 class InstallUninstallTest extends ModuleTestBase {
 
-  public static $modules = array('system_test', 'dblog', 'taxonomy');
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = array('system_test', 'dblog', 'taxonomy', 'update_test_postupdate');
 
   /**
    * Tests that a fixed set of modules can be installed and uninstalled.
@@ -105,6 +108,7 @@ public function testInstallUninstall() {
         $this->assertModuleTablesExist($module_to_install);
         $this->assertModuleConfig($module_to_install);
         $this->assertLogMessage('system', "%module module installed.", array('%module' => $module_to_install), RfcLogLevel::INFO);
+        $this->assertInstallModuleUpdates($module_to_install);
       }
 
       // Uninstall the original module, and check appropriate
@@ -188,6 +192,59 @@ protected function assertSuccessfulUninstall($module, $package = 'Core') {
     $this->assertModuleTablesDoNotExist($module);
     // Check that the module's config files no longer exist.
     $this->assertNoModuleConfig($module);
+    $this->assertUninstallModuleUpdates($module);
+  }
+
+  /**
+   * Asserts the module post update functions after install.
+   *
+   * @param string $module
+   *   The module that got installed.
+   */
+  protected function assertInstallModuleUpdates($module) {
+    /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
+    $post_update_registry = \Drupal::service('update.post_update_registry');
+    $all_update_functions = $post_update_registry->getPendingUpdateFunctions();
+    $empty_result = TRUE;
+    foreach ($all_update_functions as $function) {
+      list($function_module, ) = explode('_post_update_', $function);
+      if ($module === $function_module) {
+        $empty_result = FALSE;
+        break;
+      }
+    }
+    $this->assertTrue($empty_result, 'Ensures that no pending post update functions are available.');
+
+    $existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []);
+    switch ($module) {
+      case 'block':
+        $this->assertFalse(array_diff(['block_post_update_disable_blocks_with_missing_contexts'], $existing_updates));
+        break;
+      case 'update_test_postupdate':
+        $this->assertFalse(array_diff(['update_test_postupdate_post_update_first', 'update_test_postupdate_post_update_second', 'update_test_postupdate_post_update_test1', 'update_test_postupdate_post_update_test0'], $existing_updates));
+        break;
+    }
+  }
+
+  /**
+   * Asserts the module post update functions after uninstall.
+   *
+   * @param string $module
+   *   The module that got installed.
+   */
+  protected function assertUninstallModuleUpdates($module) {
+    /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
+    $post_update_registry = \Drupal::service('update.post_update_registry');
+    $all_update_functions = $post_update_registry->getPendingUpdateFunctions();
+
+    switch ($module) {
+      case 'block':
+        $this->assertFalse(array_intersect(['block_post_update_disable_blocks_with_missing_contexts'], $all_update_functions), 'Asserts that no pending post update functions are available.');
+
+        $existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []);
+        $this->assertFalse(array_intersect(['block_post_update_disable_blocks_with_missing_contexts'], $existing_updates), 'Asserts that no post update functions are stored in keyvalue store.');
+        break;
+    }
   }
 
 }
diff --git a/core/modules/system/src/Tests/System/StatusTest.php b/core/modules/system/src/Tests/System/StatusTest.php
index 57ef4b0..5bb9578 100644
--- a/core/modules/system/src/Tests/System/StatusTest.php
+++ b/core/modules/system/src/Tests/System/StatusTest.php
@@ -21,6 +21,11 @@ class StatusTest extends WebTestBase {
   /**
    * {@inheritdoc}
    */
+  public static $modules = ['update_test_postupdate'];
+
+  /**
+   * {@inheritdoc}
+   */
   protected function setUp() {
     parent::setUp();
 
@@ -57,6 +62,23 @@ public function testStatusPage() {
       $this->assertNoLinkByHref(Url::fromRoute('system.php')->toString());
     }
 
+    // If a module is fully installed no pending updates exists.
+    $this->assertNoText(t('Out of date'));
+
+    // 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);
+    $this->drupalGet('admin/reports/status');
+    $this->assertText(t('Out of date'));
+
+    // Now cleanup the executed post update functions.
+    drupal_set_installed_schema_version('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');
+    $this->drupalGet('admin/reports/status');
+    $this->assertText(t('Out of date'));
+
     $this->drupalGet('admin/reports/status/php');
     $this->assertResponse(200, 'The phpinfo page is reachable.');
   }
diff --git a/core/modules/system/src/Tests/Update/UpdatePathTestBase.php b/core/modules/system/src/Tests/Update/UpdatePathTestBase.php
index 1b85098..c6281d0 100644
--- a/core/modules/system/src/Tests/Update/UpdatePathTestBase.php
+++ b/core/modules/system/src/Tests/Update/UpdatePathTestBase.php
@@ -124,6 +124,13 @@
   protected $strictConfigSchema = FALSE;
 
   /**
+   * Fail the test if there are failed updates.
+   *
+   * @var bool
+   */
+  protected $checkFailedUpdates = TRUE;
+
+  /**
    * Constructs an UpdatePathTestCase object.
    *
    * @param $test_id
@@ -246,7 +253,9 @@ protected function runUpdates() {
     $this->clickLink(t('Apply pending updates'));
 
     // Ensure there are no failed updates.
-    $this->assertNoRaw('<strong>' . t('Failed:') . '</strong>');
+    if ($this->checkFailedUpdates) {
+      $this->assertNoRaw('<strong>' . t('Failed:') . '</strong>');
+    }
 
     // The config schema can be incorrect while the update functions are being
     // executed. But once the update has been completed, it needs to be valid
diff --git a/core/modules/system/src/Tests/Update/UpdatePostUpdateFailingTest.php b/core/modules/system/src/Tests/Update/UpdatePostUpdateFailingTest.php
new file mode 100644
index 0000000..0555871
--- /dev/null
+++ b/core/modules/system/src/Tests/Update/UpdatePostUpdateFailingTest.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Update\UpdatePostUpdateFailingTest.
+ */
+
+namespace Drupal\system\Tests\Update;
+
+/**
+ * Tests hook_post_update() when there are failing update hooks.
+ *
+ * @group Update
+ */
+class UpdatePostUpdateFailingTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles() {
+    $this->databaseDumpFiles = [
+      __DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz',
+      __DIR__ . '/../../../tests/fixtures/update/drupal-8.update-test-postupdate-failing-enabled.php',
+    ];
+  }
+
+  /**
+   * Tests hook_post_update_NAME().
+   */
+  public function testPostUpdate() {
+    // There are expected to be failed updates.
+    $this->checkFailedUpdates = FALSE;
+
+    $this->runUpdates();
+
+    // There should be no post update hooks registered as being run.
+    $this->assertIdentical([], \Drupal::state()->get('post_update_test_execution', []));
+
+    $key_value = \Drupal::keyValue('update__post_update');
+    $this->assertEqual([], $key_value->get('existing_updates'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doSelectionTest() {
+    // First update, should not be run since this module's update hooks fail.
+    $this->assertRaw('8001 -   This update will fail.');
+    $this->assertRaw('8002 -   A further update.');
+    $this->assertEscaped("First update, should not be run since this module's update hooks fail.");
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Update/UpdatePostUpdateTest.php b/core/modules/system/src/Tests/Update/UpdatePostUpdateTest.php
new file mode 100644
index 0000000..3431515
--- /dev/null
+++ b/core/modules/system/src/Tests/Update/UpdatePostUpdateTest.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Update\UpdatePostUpdateTest.
+ */
+
+namespace Drupal\system\Tests\Update;
+
+/**
+ * Tests hook_post_update().
+ *
+ * @group Update
+ */
+class UpdatePostUpdateTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles() {
+    $this->databaseDumpFiles = [
+      __DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz',
+      __DIR__ . '/../../../tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doSelectionTest() {
+    parent::doSelectionTest();
+
+    // Ensure that normal and post_update updates are merged together on the
+    // selection page.
+    $this->assertRaw('<ul><li>8001 -   Normal update_N() function. </li><li>First update.</li><li>Second update.</li><li>Test1 update.</li><li>Test0 update.</li></ul>');
+  }
+
+  /**
+   * Tests hook_post_update_NAME().
+   */
+  public function testPostUpdate() {
+    $this->runUpdates();
+
+    $this->assertRaw('<h3>Update first</h3>');
+    $this->assertRaw('First update');
+    $this->assertRaw('<h3>Update second</h3>');
+    $this->assertRaw('Second update');
+    $this->assertRaw('<h3>Update test1</h3>');
+    $this->assertRaw('Test1 update');
+    $this->assertRaw('<h3>Update test0</h3>');
+    $this->assertRaw('Test0 update');
+
+    $updates = [
+      'update_test_postupdate_post_update_first',
+      'update_test_postupdate_post_update_second',
+      'update_test_postupdate_post_update_test1',
+      'update_test_postupdate_post_update_test0',
+    ];
+    $this->assertIdentical($updates, \Drupal::state()->get('post_update_test_execution', []));
+
+    $key_value = \Drupal::keyValue('post_update');
+    array_unshift($updates, 'block_post_update_disable_blocks_with_missing_contexts');
+    $this->assertEqual($updates, $key_value->get('existing_updates'));
+
+    $this->drupalGet('update.php/selection');
+    $this->assertText('No pending updates.');
+  }
+
+}
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 86365a9..c316884 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -598,18 +598,31 @@ function system_requirements($phase) {
     );
 
     // Check installed modules.
+    $has_pending_updates = FALSE;
     foreach (\Drupal::moduleHandler()->getModuleList() as $module => $filename) {
       $updates = drupal_get_schema_versions($module);
       if ($updates !== FALSE) {
         $default = drupal_get_installed_schema_version($module);
         if (max($updates) > $default) {
-          $requirements['update']['severity'] = REQUIREMENT_ERROR;
-          $requirements['update']['value'] = t('Out of date');
-          $requirements['update']['description'] = t('Some modules have database schema updates to install. You should run the <a href="@update">database update script</a> immediately.', array('@update' => \Drupal::url('system.db_update')));
+          $has_pending_updates = TRUE;
           break;
         }
       }
     }
+    if (!$has_pending_updates) {
+      /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */
+      $post_update_registry = \Drupal::service('update.post_update_registry');
+      $missing_post_update_functions = $post_update_registry->getPendingUpdateFunctions();
+      if (!empty($missing_post_update_functions)) {
+        $has_pending_updates = TRUE;
+      }
+    }
+
+    if ($has_pending_updates) {
+      $requirements['update']['severity'] = REQUIREMENT_ERROR;
+      $requirements['update']['value'] = t('Out of date');
+      $requirements['update']['description'] = t('Some modules have database schema updates to install. You should run the <a href="@update">database update script</a> immediately.', array('@update' => \Drupal::url('system.db_update')));
+    }
 
     // Verify that no entity updates are pending after running every DB update.
     if (!isset($requirements['update']['severity']) && \Drupal::entityDefinitionUpdateManager()->needsUpdates()) {
diff --git a/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php b/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php
new file mode 100644
index 0000000..375b50d
--- /dev/null
+++ b/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Partial database to mimic the installation of the update_test_post_update
+ * module.
+ */
+
+use Drupal\Core\Database\Database;
+
+$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();
+
+// Update core.extension.
+$extensions = $connection->select('config')
+  ->fields('config', ['data'])
+  ->condition('collection', '')
+  ->condition('name', 'core.extension')
+  ->execute()
+  ->fetchField();
+$extensions = unserialize($extensions);
+$extensions['module']['update_test_postupdate'] = 8000;
+$connection->update('config')
+  ->fields([
+    'data' => serialize($extensions),
+  ])
+  ->condition('collection', '')
+  ->condition('name', 'core.extension')
+  ->execute();
+
diff --git a/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-failing-enabled.php b/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-failing-enabled.php
new file mode 100644
index 0000000..f19a1f1
--- /dev/null
+++ b/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-failing-enabled.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Partial database to mimic the installation of the update_test_failing module.
+ */
+
+use Drupal\Core\Database\Database;
+
+$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();
+
+// Update core.extension.
+$extensions = $connection->select('config')
+  ->fields('config', ['data'])
+  ->condition('collection', '')
+  ->condition('name', 'core.extension')
+  ->execute()
+  ->fetchField();
+$extensions = unserialize($extensions);
+$extensions['module']['update_test_failing'] = 8000;
+$connection->update('config')
+  ->fields([
+    'data' => serialize($extensions),
+  ])
+  ->condition('collection', '')
+  ->condition('name', 'core.extension')
+  ->execute();
+
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.links.menu.yml b/core/modules/system/tests/modules/menu_test/menu_test.links.menu.yml
index eecd42c..613bc92 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.links.menu.yml
+++ b/core/modules/system/tests/modules/menu_test/menu_test.links.menu.yml
@@ -83,8 +83,3 @@ menu_test.child:
 menu_test.unsafe:
   route_name: menu_test.menu_name_test
   deriver: '\Drupal\menu_test\Plugin\Derivative\MenuLinkTestWithUnsafeTitle'
-
-menu_test.access_check:
-  title: 'Test custom route access check'
-  route_name: menu_test.router_test_session
-  menu_name: account
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
index 751bdf7..b69d722 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
+++ b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
@@ -74,15 +74,6 @@ menu_test.router_test4:
   requirements:
     _access: 'TRUE'
 
-menu_test.router_test_session:
-  path: '/menu_test_access_check_session'
-  defaults:
-    _controller: '\Drupal\menu_test\TestControllers::testSession'
-  options:
-    no_cache: TRUE
-  requirements:
-    _menu_test_session_access: 'TRUE'
-
 menu_test.local_action1:
   path: '/menu-test-local-action'
   defaults:
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.services.yml b/core/modules/system/tests/modules/menu_test/menu_test.services.yml
index 81f1f50..8841320 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.services.yml
+++ b/core/modules/system/tests/modules/menu_test/menu_test.services.yml
@@ -3,8 +3,3 @@ services:
     class: Drupal\menu_test\Theme\TestThemeNegotiator
     tags:
       - { name: theme_negotiator }
-
-  access_check.menu_test_session:
-    class: Drupal\menu_test\Access\AccessCheck
-    tags:
-      - { name: access_check, applies_to: _menu_test_session_access }
diff --git a/core/modules/system/tests/modules/menu_test/src/Access/AccessCheck.php b/core/modules/system/tests/modules/menu_test/src/Access/AccessCheck.php
deleted file mode 100644
index c6b9054..0000000
--- a/core/modules/system/tests/modules/menu_test/src/Access/AccessCheck.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-/**
- * @file
- * Contains \Drupal\menu_test\Access\AccessCheck.
- */
-
-namespace Drupal\menu_test\Access;
-
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Routing\Access\AccessInterface;
-
-/**
- * Checks access based on the 'menu_test' key in session.
- */
-class AccessCheck implements AccessInterface {
-
-  /**
-   * Check to see if user accessed this page.
-   *
-   * @return \Drupal\Core\Access\AccessResultInterface
-   *   The access result.
-   */
-  public function access() {
-    if (!isset($_SESSION['menu_test'])) {
-      $result = AccessResult::allowed();
-    }
-    else {
-      $result = AccessResult::allowedIf($_SESSION['menu_test'] < 2);
-    }
-    return $result->setCacheMaxAge(0);
-  }
-
-}
diff --git a/core/modules/system/tests/modules/menu_test/src/TestControllers.php b/core/modules/system/tests/modules/menu_test/src/TestControllers.php
index 364c77b..2a91286 100644
--- a/core/modules/system/tests/modules/menu_test/src/TestControllers.php
+++ b/core/modules/system/tests/modules/menu_test/src/TestControllers.php
@@ -39,17 +39,6 @@ public function test2() {
   /**
    * Prints out test data.
    */
-  public function testSession() {
-    if (!isset($_SESSION['menu_test'])) {
-      $_SESSION['menu_test'] = 0;
-    }
-    $_SESSION['menu_test']++;
-    return ['#markup' => SafeMarkup::format('Session menu_test is @count', ['@count' => $_SESSION['menu_test']])];
-  }
-
-  /**
-   * Prints out test data.
-   */
   public function testDerived() {
     return ['#markup' => 'testDerived'];
   }
diff --git a/core/modules/system/tests/modules/module_test/module_test.post_update.php b/core/modules/system/tests/modules/module_test/module_test.post_update.php
new file mode 100644
index 0000000..2226578
--- /dev/null
+++ b/core/modules/system/tests/modules/module_test/module_test.post_update.php
@@ -0,0 +1,7 @@
+<?php
+
+/**
+ * Test post update function.
+ */
+function module_test_post_update_test() {
+}
diff --git a/core/modules/system/tests/modules/update_test_failing/update_test_failing.info.yml b/core/modules/system/tests/modules/update_test_failing/update_test_failing.info.yml
new file mode 100644
index 0000000..f6841f3
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_failing/update_test_failing.info.yml
@@ -0,0 +1,6 @@
+name: 'Update test failing'
+type: module
+description: 'Support module for update testing when an update hook is failing.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/system/tests/modules/update_test_failing/update_test_failing.install b/core/modules/system/tests/modules/update_test_failing/update_test_failing.install
new file mode 100644
index 0000000..a26c7f3
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_failing/update_test_failing.install
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * Contains a failing update hook for testing the update system.
+ */
+
+use Drupal\Core\Utility\UpdateException;
+
+/**
+ * This update will fail.
+ */
+function update_test_failing_update_8001() {
+  throw new UpdateException('This update hook is failing.');
+}
+
+/**
+ * A further update.
+ */
+function update_test_failing_update_8002() {
+  // This hook won't ever run.
+}
diff --git a/core/modules/system/tests/modules/update_test_failing/update_test_failing.post_update.php b/core/modules/system/tests/modules/update_test_failing/update_test_failing.post_update.php
new file mode 100644
index 0000000..5e55ca3
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_failing/update_test_failing.post_update.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * First update, should not be run since this module's update hooks fail.
+ */
+function update_test_failing_post_update_first() {
+  $execution = \Drupal::state()->get('post_update_test_execution', []);
+  $execution[] = __FUNCTION__;
+  \Drupal::state()->set('post_update_test_execution', $execution);
+}
diff --git a/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.info.yml b/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.info.yml
new file mode 100644
index 0000000..9dcff3a
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.info.yml
@@ -0,0 +1,3 @@
+core: 8.x
+name: Update test after
+type: module
diff --git a/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.install b/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.install
new file mode 100644
index 0000000..f754d94
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.install
@@ -0,0 +1,7 @@
+<?php
+
+/**
+ * Normal update_N() function.
+ */
+function update_test_postupdate_update_8001() {
+}
diff --git a/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.post_update.php b/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.post_update.php
new file mode 100644
index 0000000..e914cac
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.post_update.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * First update.
+ */
+function update_test_postupdate_post_update_first() {
+  $execution = \Drupal::state()->get('post_update_test_execution', []);
+  $execution[] = __FUNCTION__;
+  \Drupal::state()->set('post_update_test_execution', $execution);
+
+  return 'First update';
+}
+
+/**
+ * Second update.
+ */
+function update_test_postupdate_post_update_second() {
+  $execution = \Drupal::state()->get('post_update_test_execution', []);
+  $execution[] = __FUNCTION__;
+  \Drupal::state()->set('post_update_test_execution', $execution);
+
+  return 'Second update';
+}
+
+/**
+ * Test1 update.
+ */
+function update_test_postupdate_post_update_test1() {
+  $execution = \Drupal::state()->get('post_update_test_execution', []);
+  $execution[] = __FUNCTION__;
+  \Drupal::state()->set('post_update_test_execution', $execution);
+
+  return 'Test1 update';
+}
+
+/**
+ * Test0 update.
+ */
+function update_test_postupdate_post_update_test0() {
+  $execution = \Drupal::state()->get('post_update_test_execution', []);
+  $execution[] = __FUNCTION__;
+  \Drupal::state()->set('post_update_test_execution', $execution);
+
+  return 'Test0 update';
+}
diff --git a/core/modules/taxonomy/src/Tests/Views/ArgumentValidatorTermTest.php b/core/modules/taxonomy/src/Tests/Views/ArgumentValidatorTermTest.php
deleted file mode 100644
index 0e2853b..0000000
--- a/core/modules/taxonomy/src/Tests/Views/ArgumentValidatorTermTest.php
+++ /dev/null
@@ -1,127 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\taxonomy\Tests\Views\ArgumentValidatorTermTest.
- */
-
-namespace Drupal\taxonomy\Tests\Views;
-
-use Drupal\views\Views;
-
-/**
- * Tests the plugin of the taxonomy: term argument validator.
- *
- * @group taxonomy
- * @see Views\taxonomy\Plugin\views\argument_validator\Term
- */
-class ArgumentValidatorTermTest extends TaxonomyTestBase {
-
-  /**
-   * Stores the taxonomy term used by this test.
-   *
-   * @var array
-   */
-  protected $terms = [];
-
-  /**
-   * Stores the taxonomy names used by this test.
-   *
-   * @var array
-   */
-  protected $names = [];
-
-  /**
-   * Stores the taxonomy IDs used by this test.
-   *
-   * @var array
-   */
-  protected $ids = [];
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = ['taxonomy', 'taxonomy_test_views', 'views_test_config'];
-
-  /**
-   * Views used by this test.
-   *
-   * @var array
-   */
-  public static $testViews = ['test_argument_validator_term'];
-
-  protected function setUp() {
-    parent::setUp();
-
-    // Add three terms to the 'tags' vocabulary.
-    for ($i = 0; $i < 3; $i++) {
-      $this->terms[] = $term = $this->createTerm();
-      $this->names[] = $term->label();
-      $this->ids[] = $term->id();
-    }
-  }
-
-  /**
-   * Tests the term argument validator plugin.
-   */
-  public function testArgumentValidatorTerm() {
-    $view = Views::getView('test_argument_validator_term');
-    $view->initHandlers();
-
-
-    // Test the single validator for term IDs.
-    $view->argument['tid']->validator->options['type'] = 'tid';
-
-    // Pass in a single valid term.
-    foreach ($this->terms as $term) {
-      $this->assertTrue($view->argument['tid']->setArgument($term->id()));
-      $this->assertEqual($view->argument['tid']->getTitle(), $term->label());
-      $view->argument['tid']->validated_title = NULL;
-      $view->argument['tid']->argument_validated = NULL;
-    }
-
-    // Pass in a invalid term.
-    $this->assertFalse($view->argument['tid']->setArgument(rand(1000, 10000)));
-    $this->assertEqual('', $view->argument['tid']->getTitle());
-    $view->argument['tid']->validated_title = NULL;
-    $view->argument['tid']->argument_validated = NULL;
-
-
-    // Test the multiple validator for term IDs.
-    $view->argument['tid']->validator->options['type'] = 'tids';
-    $view->argument['tid']->options['break_phrase'] = TRUE;
-
-    // Pass in a single term.
-    $this->assertTrue($view->argument['tid']->setArgument($this->terms[0]->id()));
-    $this->assertEqual($view->argument['tid']->getTitle(), $this->terms[0]->label());
-    $view->argument['tid']->validated_title = NULL;
-    $view->argument['tid']->argument_validated = NULL;
-
-    // Check for multiple valid terms separated by commas.
-    $this->assertTrue($view->argument['tid']->setArgument(implode(',', $this->ids)));
-    $this->assertEqual($view->argument['tid']->getTitle(), implode(', ', $this->names));
-    $view->argument['tid']->validated_title = NULL;
-    $view->argument['tid']->argument_validated = NULL;
-
-    // Check for multiple valid terms separated by plus signs.
-    $this->assertTrue($view->argument['tid']->setArgument(implode('+', $this->ids)));
-    $this->assertEqual($view->argument['tid']->getTitle(), implode(' + ', $this->names));
-    $view->argument['tid']->validated_title = NULL;
-    $view->argument['tid']->argument_validated = NULL;
-
-    // Check for a single invalid term.
-    $this->assertFalse($view->argument['tid']->setArgument(rand(1000, 10000)));
-    $this->assertEqual('', $view->argument['tid']->getTitle());
-    $view->argument['tid']->validated_title = NULL;
-    $view->argument['tid']->argument_validated = NULL;
-
-    // Check for multiple invalid terms.
-    $this->assertFalse($view->argument['tid']->setArgument(implode(',', [rand(1000, 10000), rand(1000, 10000)])));
-    $this->assertEqual('', $view->argument['tid']->getTitle());
-    $view->argument['tid']->validated_title = NULL;
-    $view->argument['tid']->argument_validated = NULL;
-  }
-
-}
diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
index a049afc..534925c 100644
--- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
+++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Plugin\DependentPluginInterface;
 use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\Unicode;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheableMetadata;
@@ -1129,7 +1130,7 @@ public function optionsSummary(&$categories, &$options) {
       );
     }
 
-    $display_comment = views_ui_truncate($this->getOption('display_comment'), 80);
+    $display_comment = Unicode::substr($this->getOption('display_comment'), 0, 10);
     $options['display_comment'] = array(
       'category' => 'other',
       'title' => $this->t('Administrative comment'),
diff --git a/core/modules/views/src/Tests/Plugin/ArgumentValidatorTest.php b/core/modules/views/src/Tests/Plugin/ArgumentValidatorTest.php
index 08b8984..ecbcf51 100644
--- a/core/modules/views/src/Tests/Plugin/ArgumentValidatorTest.php
+++ b/core/modules/views/src/Tests/Plugin/ArgumentValidatorTest.php
@@ -9,7 +9,6 @@
 
 use Drupal\views\Tests\ViewKernelTestBase;
 use Drupal\views\Views;
-use Drupal\views_test_data\Plugin\views\argument_validator\ArgumentValidatorTest as ArgumentValidatorTestPlugin;
 
 /**
  * Tests Views argument validators.
@@ -23,7 +22,7 @@ class ArgumentValidatorTest extends ViewKernelTestBase {
    *
    * @var array
    */
-  public static $testViews = ['test_view_argument_validate_numeric', 'test_view'];
+  public static $testViews = array('test_view_argument_validate_numeric');
 
   function testArgumentValidateNumeric() {
     $view = Views::getView('test_view_argument_validate_numeric');
@@ -34,37 +33,4 @@ function testArgumentValidateNumeric() {
     $this->assertTrue($view->argument['null']->validateArgument(12));
   }
 
-  /**
-   * Tests the argument validator test plugin.
-   *
-   * @see Drupal\views_test_data\Plugin\views\argument_validator\ArgumentValidatorTest
-   */
-  public function testArgumentValidatorPlugin() {
-    $view = Views::getView('test_view');
-
-    // Add a new argument and set the test plugin for the argument_validator.
-    $options = [
-      'specify_validation' => TRUE,
-      'validate' => [
-        'type' => 'argument_validator_test'
-      ]
-    ];
-    $id = $view->addHandler('default', 'argument', 'views_test_data', 'name', $options);
-    $view->initHandlers();
-
-    $test_value = $this->randomMachineName();
-
-    $argument = $view->argument[$id];
-    $argument->options['validate_options']['test_value'] = $test_value;
-    $this->assertFalse($argument->validateArgument($this->randomMachineName()), 'A random value does not validate.');
-    // Reset internal flag.
-    $argument->argument_validated = NULL;
-    $this->assertTrue($argument->validateArgument($test_value), 'The right argument validates.');
-
-    $plugin = $argument->getPlugin('argument_validator');
-    $this->assertTrue($plugin instanceof ArgumentValidatorTestPlugin, 'The correct argument validator plugin is used.');
-    $this->assertFalse($plugin->validateArgument($this->randomMachineName()), 'A random value does not validate.');
-    $this->assertTrue($plugin->validateArgument($test_value), 'The right argument validates.');
-  }
-
 }
diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_argument_validator_term.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_argument_validator_term.yml
deleted file mode 100644
index 18e28d1..0000000
--- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_argument_validator_term.yml
+++ /dev/null
@@ -1,213 +0,0 @@
-langcode: en
-status: true
-dependencies:
-  module:
-    - node
-    - taxonomy
-    - user
-id: test_argument_validator_term
-label: test_argument_validator_term
-module: views
-description: ''
-tag: ''
-base_table: node_field_data
-base_field: nid
-core: 8.x
-display:
-  default:
-    display_plugin: default
-    id: default
-    display_title: Master
-    position: 0
-    display_options:
-      access:
-        type: perm
-        options:
-          perm: 'access content'
-      cache:
-        type: none
-        options: {  }
-      query:
-        type: views_query
-        options:
-          disable_sql_rewrite: false
-          distinct: false
-          replica: false
-          query_comment: ''
-          query_tags: {  }
-      exposed_form:
-        type: basic
-        options:
-          submit_button: Apply
-          reset_button: false
-          reset_button_label: Reset
-          exposed_sorts_label: 'Sort by'
-          expose_sort_order: true
-          sort_asc_label: Asc
-          sort_desc_label: Desc
-      pager:
-        type: full
-        options:
-          items_per_page: 10
-          offset: 0
-          id: 0
-          total_pages: null
-          expose:
-            items_per_page: false
-            items_per_page_label: 'Items per page'
-            items_per_page_options: '5, 10, 25, 50'
-            items_per_page_options_all: false
-            items_per_page_options_all_label: '- All -'
-            offset: false
-            offset_label: Offset
-          tags:
-            previous: '‹ previous'
-            next: 'next ›'
-            first: '« first'
-            last: 'last »'
-          quantity: 9
-      style:
-        type: default
-        options:
-          grouping: {  }
-          row_class: ''
-          default_row_class: true
-          uses_fields: false
-      row:
-        type: fields
-        options:
-          inline: {  }
-          separator: ''
-          hide_empty: false
-          default_field_elements: true
-      fields:
-        title:
-          id: title
-          table: node_field_data
-          field: title
-          entity_type: node
-          entity_field: title
-          label: ''
-          alter:
-            alter_text: false
-            make_link: false
-            absolute: false
-            trim: false
-            word_boundary: false
-            ellipsis: false
-            strip_tags: false
-            html: false
-          hide_empty: false
-          empty_zero: false
-          settings:
-            link_to_entity: true
-          plugin_id: field
-          relationship: none
-          group_type: group
-          admin_label: ''
-          exclude: false
-          element_type: ''
-          element_class: ''
-          element_label_type: ''
-          element_label_class: ''
-          element_label_colon: true
-          element_wrapper_type: ''
-          element_wrapper_class: ''
-          element_default_classes: true
-          empty: ''
-          hide_alter_empty: true
-          click_sort_column: value
-          type: string
-          group_column: value
-          group_columns: {  }
-          group_rows: true
-          delta_limit: 0
-          delta_offset: 0
-          delta_reversed: false
-          delta_first_last: false
-          multi_type: separator
-          separator: ', '
-          field_api_classes: false
-      filters:
-        status:
-          value: true
-          table: node_field_data
-          field: status
-          plugin_id: boolean
-          entity_type: node
-          entity_field: status
-          id: status
-          expose:
-            operator: ''
-          group: 1
-      sorts:
-        created:
-          id: created
-          table: node_field_data
-          field: created
-          order: DESC
-          entity_type: node
-          entity_field: created
-          plugin_id: date
-          relationship: none
-          group_type: group
-          admin_label: ''
-          exposed: false
-          expose:
-            label: ''
-          granularity: second
-      header: {  }
-      footer: {  }
-      empty: {  }
-      relationships: {  }
-      arguments:
-        tid:
-          id: tid
-          table: taxonomy_index
-          field: tid
-          relationship: none
-          group_type: group
-          admin_label: ''
-          default_action: ignore
-          exception:
-            value: all
-            title_enable: false
-            title: All
-          title_enable: true
-          title: '%1'
-          default_argument_type: fixed
-          default_argument_options:
-            argument: ''
-          default_argument_skip_url: false
-          summary_options:
-            base_path: ''
-            count: true
-            items_per_page: 25
-            override: false
-          summary:
-            sort_order: asc
-            number_of_records: 0
-            format: default_summary
-          specify_validation: true
-          validate:
-            type: 'entity:taxonomy_term'
-            fail: 'access denied'
-          validate_options:
-            operation: view
-            multiple: 1
-            bundles: {  }
-            access: false
-          break_phrase: false
-          add_table: false
-          require_value: false
-          reduce_duplicates: false
-          plugin_id: taxonomy_index_tid
-      display_extenders: {  }
-    cache_metadata:
-      contexts:
-        - 'languages:language_content'
-        - 'languages:language_interface'
-        - url
-        - 'url.query_args.pagers:0'
-        - 'user.node_grants:view'
-      cacheable: false
diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/argument_validator/ArgumentValidatorTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/argument_validator/ArgumentValidatorTest.php
index 86ecacf..83e6f3c 100644
--- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/argument_validator/ArgumentValidatorTest.php
+++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/argument_validator/ArgumentValidatorTest.php
@@ -27,21 +27,4 @@ public function calculateDependencies() {
     ];
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function defineOptions() {
-    $options = parent::defineOptions();
-    $options['test_value'] = ['default' => ''];
-
-    return $options;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateArgument($arg) {
-    return $arg == $this->options['test_value'];
-  }
-
 }
diff --git a/core/modules/views_ui/src/Tests/ViewEditTest.php b/core/modules/views_ui/src/Tests/ViewEditTest.php
index b68dee0..3595aa2 100644
--- a/core/modules/views_ui/src/Tests/ViewEditTest.php
+++ b/core/modules/views_ui/src/Tests/ViewEditTest.php
@@ -45,18 +45,13 @@ public function testDeleteLink() {
   }
 
   /**
-   * Tests the machine name and administrative comment forms.
+   * Tests the machine name form.
    */
-  public function testOtherOptions() {
+  public function testMachineNameOption() {
     $this->drupalGet('admin/structure/views/view/test_view');
     // Add a new attachment display.
     $this->drupalPostForm(NULL, array(), 'Add Attachment');
 
-    // Test that a long administrative comment is truncated.
-    $edit = array('display_comment' => 'one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen');
-    $this->drupalPostForm('admin/structure/views/nojs/display/test_view/attachment_1/display_comment', $edit, 'Apply');
-    $this->assertText('one two three four five six seven eight nine ten eleven twelve thirteen fourteen...');
-
     // Change the machine name for the display from page_1 to test_1.
     $edit = array('display_id' => 'test_1');
     $this->drupalPostForm('admin/structure/views/nojs/display/test_view/attachment_1/display_id', $edit, 'Apply');
diff --git a/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php b/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php
new file mode 100644
index 0000000..5de6780
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php
@@ -0,0 +1,301 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Update\UpdateRegistryTest.
+ */
+
+namespace Drupal\Tests\Core\Update;
+
+use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
+use Drupal\Core\Site\Settings;
+use Drupal\Core\Update\UpdateRegistry;
+use Drupal\Tests\UnitTestCase;
+use org\bovigo\vfs\vfsStream;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Update\UpdateRegistry
+ * @group Update
+ *
+ * Note we load code, so we should better run in isolation.
+ *
+ * @runTestsInSeparateProcesses
+ */
+class UpdateRegistryTest extends UnitTestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $settings = [];
+    $settings['extension_discovery_scan_tests'] = TRUE;
+    new Settings($settings);
+  }
+
+  /**
+   * Sets up some modules with some update functions.
+   */
+  protected function setupBasicModules() {
+    $info_a = <<<'EOS'
+type: module
+name: Module A
+core: 8.x
+EOS;
+
+    $info_b = <<<'EOS'
+type: module
+name: Module B
+core: 8.x
+EOS;
+
+    $module_a = <<<'EOS'
+<?php
+
+/**
+ * Module A update A.
+ */
+function module_a_post_update_a() {
+}
+
+/**
+ * Module A update B.
+ */
+function module_a_post_update_b() {
+}
+
+EOS;
+    $module_b = <<<'EOS'
+<?php
+
+/**
+ * Module B update A.
+ */
+function module_b_post_update_a() {
+}
+
+EOS;
+    vfsStream::setup('drupal');
+    vfsStream::create([
+      'sites' => [
+        'default' => [
+          'modules' => [
+            'module_a' => [
+              'module_a.post_update.php' => $module_a,
+              'module_a.info.yml' => $info_a
+            ],
+            'module_b' => [
+              'module_b.post_update.php' => $module_b,
+              'module_b.info.yml' => $info_b
+            ],
+          ]
+        ]
+      ],
+    ]);
+  }
+
+  /**
+   * @covers ::getPendingUpdateFunctions
+   */
+  public function testGetPendingUpdateFunctionsNoExistingUpdates() {
+    $this->setupBasicModules();
+
+    $key_value = $this->prophesize(KeyValueStoreInterface::class);
+    $key_value->get('existing_updates', [])->willReturn([]);
+    $key_value = $key_value->reveal();
+
+    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
+      'module_a',
+      'module_b'
+    ], $key_value, FALSE);
+
+    $this->assertEquals([
+      'module_a_post_update_a',
+      'module_a_post_update_b',
+      'module_b_post_update_a'
+    ], $update_registry->getPendingUpdateFunctions());
+  }
+
+  /**
+   * @covers ::getPendingUpdateFunctions
+   */
+  public function testGetPendingUpdateFunctionsWithLoadedModulesButNotEnabled() {
+    $this->setupBasicModules();
+
+    $key_value = $this->prophesize(KeyValueStoreInterface::class);
+    $key_value->get('existing_updates', [])->willReturn([]);
+    $key_value = $key_value->reveal();
+
+    // Preload modules to ensure that ::getAvailableUpdateFunctions filters out
+    // not enabled modules.
+    include_once 'vfs://drupal/sites/default/modules/module_a/module_a.post_update.php';
+    include_once 'vfs://drupal/sites/default/modules/module_b/module_b.post_update.php';
+
+    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
+      'module_a',
+    ], $key_value, FALSE);
+
+    $this->assertEquals([
+      'module_a_post_update_a',
+      'module_a_post_update_b',
+    ], $update_registry->getPendingUpdateFunctions());
+  }
+
+  /**
+   * @covers ::getPendingUpdateFunctions
+   */
+  public function testGetPendingUpdateFunctionsExistingUpdates() {
+    $this->setupBasicModules();
+
+    $key_value = $this->prophesize(KeyValueStoreInterface::class);
+    $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_a']);
+    $key_value = $key_value->reveal();
+
+    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
+      'module_a',
+      'module_b'
+    ], $key_value, FALSE);
+
+    $this->assertEquals(array_values([
+      'module_a_post_update_b',
+      'module_b_post_update_a'
+    ]), array_values($update_registry->getPendingUpdateFunctions()));
+
+  }
+
+  /**
+   * @covers ::getPendingUpdateInformation
+   */
+  public function testGetPendingUpdateInformation() {
+    $this->setupBasicModules();
+
+    $key_value = $this->prophesize(KeyValueStoreInterface::class);
+    $key_value->get('existing_updates', [])->willReturn([]);
+    $key_value = $key_value->reveal();
+
+    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
+      'module_a',
+      'module_b'
+    ], $key_value, FALSE);
+
+    $expected = [];
+    $expected['module_a']['pending']['a'] = 'Module A update A.';
+    $expected['module_a']['pending']['b'] = 'Module A update B.';
+    $expected['module_a']['start'] = 'a';
+    $expected['module_b']['pending']['a'] = 'Module B update A.';
+    $expected['module_b']['start'] = 'a';
+
+    $this->assertEquals($expected, $update_registry->getPendingUpdateInformation());
+  }
+
+  /**
+   * @covers ::getPendingUpdateInformation
+   */
+  public function testGetPendingUpdateInformationWithExistingUpdates() {
+    $this->setupBasicModules();
+
+    $key_value = $this->prophesize(KeyValueStoreInterface::class);
+    $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_a']);
+    $key_value = $key_value->reveal();
+
+    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
+      'module_a',
+      'module_b'
+    ], $key_value, FALSE);
+
+    $expected = [];
+    $expected['module_a']['pending']['b'] = 'Module A update B.';
+    $expected['module_a']['start'] = 'b';
+    $expected['module_b']['pending']['a'] = 'Module B update A.';
+    $expected['module_b']['start'] = 'a';
+
+    $this->assertEquals($expected, $update_registry->getPendingUpdateInformation());
+  }
+
+  /**
+   * @covers ::getModuleUpdateFunctions
+   */
+  public function testGetModuleUpdateFunctions() {
+    $this->setupBasicModules();
+    $key_value = $this->prophesize(KeyValueStoreInterface::class)->reveal();
+
+    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
+      'module_a',
+      'module_b'
+    ], $key_value, FALSE);
+
+    $this->assertEquals(['module_a_post_update_a', 'module_a_post_update_b'], array_values($update_registry->getModuleUpdateFunctions('module_a')));
+    $this->assertEquals(['module_b_post_update_a'], array_values($update_registry->getModuleUpdateFunctions('module_b')));
+  }
+
+  /**
+   * @covers ::registerInvokedUpdates
+   */
+  public function testRegisterInvokedUpdatesWithoutExistingUpdates() {
+    $this->setupBasicModules();
+    $key_value = $this->prophesize(KeyValueStoreInterface::class);
+    $key_value->get('existing_updates', [])->willReturn([]);
+    $key_value->set('existing_updates', ['module_a_post_update_a'])->willReturn(NULL);
+    $key_value = $key_value->reveal();
+
+    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
+      'module_a',
+      'module_b'
+    ], $key_value, FALSE);
+    $update_registry->registerInvokedUpdates(['module_a_post_update_a']);
+  }
+
+  /**
+   * @covers ::registerInvokedUpdates
+   */
+  public function testRegisterInvokedUpdatesWithMultiple() {
+    $this->setupBasicModules();
+    $key_value = $this->prophesize(KeyValueStoreInterface::class);
+    $key_value->get('existing_updates', [])->willReturn([]);
+    $key_value->set('existing_updates', ['module_a_post_update_a', 'module_a_post_update_b'])->willReturn(NULL);
+    $key_value = $key_value->reveal();
+
+    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
+      'module_a',
+      'module_b'
+    ], $key_value, FALSE);
+    $update_registry->registerInvokedUpdates(['module_a_post_update_a', 'module_a_post_update_b']);
+  }
+
+  /**
+   * @covers ::registerInvokedUpdates
+   */
+  public function testRegisterInvokedUpdatesWithExistingUpdates() {
+    $this->setupBasicModules();
+    $key_value = $this->prophesize(KeyValueStoreInterface::class);
+    $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_b']);
+    $key_value->set('existing_updates', ['module_a_post_update_b', 'module_a_post_update_a'])->willReturn(NULL);
+    $key_value = $key_value->reveal();
+
+    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
+      'module_a',
+      'module_b'
+    ], $key_value, FALSE);
+    $update_registry->registerInvokedUpdates(['module_a_post_update_a']);
+  }
+
+  /**
+   * @covers ::filterOutInvokedUpdatesByModule
+   */
+  public function testFilterOutInvokedUpdatesByModule() {
+    $this->setupBasicModules();
+    $key_value = $this->prophesize(KeyValueStoreInterface::class);
+    $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_b', 'module_a_post_update_a', 'module_b_post_update_a']);
+    $key_value->set('existing_updates', ['module_b_post_update_a'])->willReturn(NULL);
+    $key_value = $key_value->reveal();
+
+    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
+      'module_a',
+      'module_b'
+    ], $key_value, FALSE);
+
+    $update_registry->filterOutInvokedUpdatesByModule('module_a');
+  }
+
+}
