diff --git a/core/core.services.yml b/core/core.services.yml
index 090d8f3..7bbcedb 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1522,6 +1522,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/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..3c1c228 100644
--- a/core/lib/Drupal/Core/Extension/module.api.php
+++ b/core/lib/Drupal/Core/Extension/module.api.php
@@ -660,6 +660,71 @@ 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) {
+  // Example of updating some content.
+  $node = \Drupal\node\Entity\Node::load(123);
+  $node->setTitle('foo');
+  $node->save();
+
+  $result = t('Node %nid saved', ['%nid' => $node->id()]);
+
+  // Example of disabling blocks with missing condition contexts. Note: The
+  // block itself is in a state which is valid at that point.
+  // @see block_update_8001()
+  // @see block_post_update_disable_blocks_with_missing_contexts()
+  $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();
+    }
+  }
+
+  return $result;
+}
+
+/**
  * 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/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/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/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');
+  }
+
+}
