diff --git a/automatic_updates.module b/automatic_updates.module
index 2b020f3af540af196ac0dbdc2002faa0648f3ac3..fd71e968d7fb265487612715d3ba5a5c86defabe 100644
--- a/automatic_updates.module
+++ b/automatic_updates.module
@@ -6,8 +6,10 @@
  */
 
 use Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface;
-use Drupal\update\UpdateManagerInterface;
 use Drupal\Core\Url;
+use Drupal\update\UpdateManagerInterface;
+use Symfony\Component\Process\PhpExecutableFinder;
+use Symfony\Component\Process\Process;
 
 /**
  * Implements hook_page_top().
@@ -91,20 +93,35 @@ function automatic_updates_cron() {
     $not_recommended_version = $projects['drupal']['status'] !== UpdateManagerInterface::CURRENT;
     $security_update = in_array($projects['drupal']['status'], [UpdateManagerInterface::NOT_SECURE, UpdateManagerInterface::REVOKED], TRUE);
     $recommended_release = isset($projects['drupal']['releases'][$projects['drupal']['recommended']]) ? $projects['drupal']['releases'][$projects['drupal']['recommended']] : NULL;
-    $major_upgrade = isset($recommended_release['version_major']) ? $recommended_release['version_major'] !== $projects['drupal']['existing_major'] : TRUE;
-    // Don't automatically update major version bumps or a dev version of core.
-    if (!$major_upgrade && $config->get('enable_cron_security_updates')) {
-      if ($not_recommended_version && $security_update) {
+    $existing_minor_version = explode('.', \Drupal::VERSION, -1);
+    $recommended_minor_version = explode('.', $recommended_release['version'], -1);
+    $major_upgrade = $existing_minor_version !== $recommended_minor_version;
+    if ($major_upgrade) {
+      foreach (range(1, 30) as $point_version) {
+        $potential_version = implode('.', array_merge($existing_minor_version, (array) $point_version));
+        if (isset($available['drupal']['releases'][$potential_version])) {
+          $recommended_release = $available['drupal']['releases'][$potential_version];
+        }
+        else {
+          break;
+        }
+      }
+    }
+    // Don't automatically update major version bumps or from/to same version.
+    if ($not_recommended_version && $projects['drupal']['existing_version'] !== $recommended_release['version']) {
+      if ($config->get('enable_cron_security_updates')) {
+        if ($security_update) {
+          /** @var \Drupal\automatic_updates\Services\UpdateInterface $updater */
+          $updater = \Drupal::service('automatic_updates.update');
+          $updater->update('drupal', 'core', \Drupal::VERSION, $recommended_release['version']);
+        }
+      }
+      else {
         /** @var \Drupal\automatic_updates\Services\UpdateInterface $updater */
         $updater = \Drupal::service('automatic_updates.update');
         $updater->update('drupal', 'core', \Drupal::VERSION, $recommended_release['version']);
       }
     }
-    elseif (!$major_upgrade && $not_recommended_version) {
-      /** @var \Drupal\automatic_updates\Services\UpdateInterface $updater */
-      $updater = \Drupal::service('automatic_updates.update');
-      $updater->update('drupal', 'core', \Drupal::VERSION, $recommended_release['version']);
-    }
   }
 
   /** @var \Drupal\automatic_updates\Services\NotifyInterface $notify */
@@ -141,3 +158,30 @@ function automatic_updates_mail($key, &$message, $params) {
   $message['subject'] = $params['subject'];
   $message['body'][] = $renderer->render($params['body']);
 }
+
+/**
+ * Helper method to execute console command.
+ *
+ * @param string $command_argument
+ *   The command argument.
+ *
+ * @return \Symfony\Component\Process\Process
+ *   The console command process.
+ */
+function automatic_updates_console_command($command_argument) {
+  $module_path = drupal_get_path('module', 'automatic_updates');
+  $command = [
+    (new PhpExecutableFinder())->find(),
+    $module_path . '/scripts/automatic_update_tools',
+    $command_argument,
+    '--script-filename',
+    \Drupal::request()->server->get('SCRIPT_FILENAME'),
+    '--base-url',
+    \Drupal::request()->getBaseUrl(),
+    '--base-path',
+    \Drupal::request()->getBasePath(),
+  ];
+  $process = new Process($command, (string) \Drupal::root(), NULL, NULL, NULL);
+  $process->run();
+  return $process;
+}
diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml
index 35402f431bf4a7e29a636b1309f48dd92090a386..53dab8ea0ae4a0b7ccceb0a49342661c7ba68e1c 100644
--- a/automatic_updates.services.yml
+++ b/automatic_updates.services.yml
@@ -43,6 +43,9 @@ services:
       - '@file_system'
       - '@http_client'
       - '@app.root'
+  plugin.manager.database_update_handler:
+    class: Drupal\automatic_updates\DatabaseUpdateHandlerPluginManager
+    parent: default_plugin_manager
 
   automatic_updates.readiness_checker:
     class: Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManager
@@ -92,6 +95,8 @@ services:
       - { name: readiness_checker, category: warning}
   automatic_updates.pending_db_updates:
     class: Drupal\automatic_updates\ReadinessChecker\PendingDbUpdates
+    arguments:
+      - '@update.post_update_registry'
     tags:
       - { name: readiness_checker, category: error}
   automatic_updates.missing_project_info:
diff --git a/config/install/automatic_updates.settings.yml b/config/install/automatic_updates.settings.yml
index d3cc904507bb1658be5e59f96cd44d88000c4faf..a6d4891a6f8a70d2139004ed9c7bee9525b97e40 100644
--- a/config/install/automatic_updates.settings.yml
+++ b/config/install/automatic_updates.settings.yml
@@ -8,3 +8,7 @@ ignored_paths: "modules/*\nthemes/*\nprofiles/*"
 download_uri: 'https://www.drupal.org/in-place-updates'
 enable_cron_updates: false
 enable_cron_security_updates: false
+database_update_handling:
+  - maintenance_mode_activate
+  - execute_updates
+  - maintenance_mode_disactivate
diff --git a/config/schema/automatic_updates.schema.yml b/config/schema/automatic_updates.schema.yml
index d3197e60380b5bb7b522c992a1ed6146f15293ab..77515d7334deba2675c680ab1b7d08136a8c93a5 100644
--- a/config/schema/automatic_updates.schema.yml
+++ b/config/schema/automatic_updates.schema.yml
@@ -32,3 +32,9 @@ automatic_updates.settings:
     enable_cron_security_updates:
       type: boolean
       label: 'Enable automatic updates for security releases via cron'
+    database_update_handling:
+      type: sequence
+      label: 'Database update handling'
+      sequence:
+        type: string
+        label: 'Tagged service to handle database updates'
diff --git a/scripts/automatic_update_tools b/scripts/automatic_update_tools
new file mode 100644
index 0000000000000000000000000000000000000000..78da7dbeb9ef7d650d5573f3279647a6ef7e6204
--- /dev/null
+++ b/scripts/automatic_update_tools
@@ -0,0 +1,41 @@
+#!/usr/bin/env php
+<?php
+
+/**
+ * @file
+ * Provides helper commands for automatic updates.
+ *
+ * This must be a separate application so class caches aren't an issue during
+ * in place updates.
+ */
+
+use Drupal\automatic_updates\Command\CacheRebuild;
+use Drupal\automatic_updates\Command\DatabaseUpdate;
+use Drupal\automatic_updates\Command\DatabaseUpdateStatus;
+use Symfony\Component\Console\Application;
+
+if (PHP_SAPI !== 'cli') {
+  return;
+}
+
+// Set up autoloader
+$loader = false;
+if (file_exists($autoloader = __DIR__ . '/../../../../autoload.php')
+  || file_exists($autoloader = __DIR__ . '/../../../../../autoload.php')
+  || file_exists($autoloader = __DIR__ . '/../../../autoload.php')
+) {
+  /** @var \Composer\Autoload\ClassLoader $loader */
+  $loader = require_once $autoloader;
+  // Drupal's autoloader doesn't bootstrap this module's classes yet. Do so
+  // manually.
+  $loader->addPsr4('Drupal\\automatic_updates\\', __DIR__ . '/../src');
+}
+else {
+  throw new \RuntimeException('Could not locate autoload.php; __DIR__ is ' . __DIR__);
+}
+
+$application = new Application('automatic_update_tools', 'stable');
+$application->add(new CacheRebuild($loader));
+$application->add(new DatabaseUpdate($loader));
+$application->add(new DatabaseUpdateStatus($loader));
+$application->run();
diff --git a/src/Annotation/DatabaseUpdateHandler.php b/src/Annotation/DatabaseUpdateHandler.php
new file mode 100644
index 0000000000000000000000000000000000000000..8e881e8c2ef49a6a0c6c6a3532f5ab3574870740
--- /dev/null
+++ b/src/Annotation/DatabaseUpdateHandler.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\automatic_updates\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a DatabaseUpdateHandler annotation object.
+ *
+ * Plugin Namespace: Plugin\DatabaseUpdateHandler.
+ *
+ * For a working example, see
+ * \Drupal\automatic_updates\Plugin\DatabaseUpdateHandler\MaintenanceMode.
+ *
+ * @see \Drupal\automatic_updates\DatabaseUpdateHandlerInterface
+ * @see \Drupal\automatic_updates\DatabaseUpdateHandlerPluginBase
+ * @see \Drupal\automatic_updates\DatabaseUpdateHandlerPluginManager
+ * @see hook_database_update_handler_plugin_info_alter()
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+final class DatabaseUpdateHandler extends Plugin {
+
+  /**
+   * The ID of the handler, should match the service name.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The name of the handler.
+   *
+   * @var string
+   */
+  public $label;
+
+}
diff --git a/src/Command/BaseCommand.php b/src/Command/BaseCommand.php
new file mode 100644
index 0000000000000000000000000000000000000000..268d029fc5acbbe58338da5d120e256bdbda1ac8
--- /dev/null
+++ b/src/Command/BaseCommand.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\automatic_updates\Command;
+
+use Drupal\Core\DrupalKernel;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Base command class.
+ */
+class BaseCommand extends Command {
+
+  /**
+   * The class loader.
+   *
+   * @var object
+   */
+  protected $classLoader;
+
+  /**
+   * Constructs a new InstallCommand command.
+   *
+   * @param object $class_loader
+   *   The class loader.
+   */
+  public function __construct($class_loader) {
+    $this->classLoader = $class_loader;
+    parent::__construct();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function configure() {
+    parent::configure();
+    $this->addOption('script-filename', NULL, InputOption::VALUE_REQUIRED, 'The script filename')
+      ->addOption('base-url', NULL, InputOption::VALUE_REQUIRED, 'The base URL, i.e. http://example.com/index.php')
+      ->addOption('base-path', NULL, InputOption::VALUE_REQUIRED, 'The base path, i.e. http://example.com');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function execute(InputInterface $input, OutputInterface $output) {
+    $this->bootstrapDrupal($input);
+  }
+
+  /**
+   * Bootstrap Drupal.
+   *
+   * @param \Symfony\Component\Console\Input\InputInterface $input
+   *   The input.
+   */
+  protected function bootstrapDrupal(InputInterface $input) {
+    $kernel = new DrupalKernel('prod', $this->classLoader);
+    $script_filename = $input->getOption('script-filename');
+    $base_url = $input->getOption('base-url');
+    $base_path = $input->getOption('base-path');
+    $server = [
+      'SCRIPT_FILENAME' => $script_filename,
+      'SCRIPT_NAME' => $base_url,
+    ];
+    $request = Request::create($base_path, 'GET', [], [], [], $server);
+    $kernel->handle($request);
+  }
+
+}
diff --git a/src/Command/CacheRebuild.php b/src/Command/CacheRebuild.php
new file mode 100644
index 0000000000000000000000000000000000000000..5bc2c594fb9c73656c191ce1f1392a4011e6b343
--- /dev/null
+++ b/src/Command/CacheRebuild.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\automatic_updates\Command;
+
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Cache rebuild command.
+ */
+class CacheRebuild extends BaseCommand {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function configure() {
+    parent::configure();
+    $this->setName('cache:rebuild')
+      ->setAliases(['cr, rebuild'])
+      ->setDescription('Rebuild a Drupal site and clear all its caches.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function execute(InputInterface $input, OutputInterface $output) {
+    parent::execute($input, $output);
+    drupal_flush_all_caches();
+    $output->writeln('Cache rebuild complete.');
+  }
+
+}
diff --git a/src/Command/DatabaseUpdate.php b/src/Command/DatabaseUpdate.php
new file mode 100644
index 0000000000000000000000000000000000000000..c88cc62fbe7ef81a1e1bff5f1e3b24cfd62c476a
--- /dev/null
+++ b/src/Command/DatabaseUpdate.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace Drupal\automatic_updates\Command;
+
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Database update command.
+ */
+class DatabaseUpdate extends BaseCommand {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function configure() {
+    parent::configure();
+    $this->setName('updatedb')
+      ->setDescription('Apply any database updates required (as with running update.php).')
+      ->setAliases(['updb']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function execute(InputInterface $input, OutputInterface $output) {
+    parent::execute($input, $output);
+    $pending_updates = \Drupal::service('automatic_updates.pending_db_updates')
+      ->run();
+    if ($pending_updates) {
+      $output->writeln('Started database updates.');
+      $this->executeDatabaseUpdates();
+      $output->writeln('Finished database updates.');
+    }
+    else {
+      $output->writeln('No database updates required.');
+    }
+  }
+
+  /**
+   * Execute all outstanding database updates.
+   */
+  protected function executeDatabaseUpdates() {
+    require_once DRUPAL_ROOT . '/core/includes/install.inc';
+    require_once DRUPAL_ROOT . '/core/includes/update.inc';
+    $logger = \Drupal::logger('automatic_updates');
+    drupal_load_updates();
+    $start = $dependency_map = $operations = [];
+    foreach (update_get_update_list() as $module => $update) {
+      $start[$module] = $update['start'];
+    }
+    $updates = update_resolve_dependencies($start);
+    foreach ($updates as $function => $update) {
+      $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : [];
+    }
+    foreach ($updates as $function => $update) {
+      if ($update['allowed']) {
+        // Set the installed version of each module so updates will start at the
+        // correct place. (The updates are already sorted, so we can simply base
+        // this on the first one we come across in the above foreach loop.)
+        if (isset($start[$update['module']])) {
+          drupal_set_installed_schema_version($update['module'], $update['number'] - 1);
+          unset($start[$update['module']]);
+        }
+        $this->executeDatabaseUpdate('update_do_one', [
+          $update['module'],
+          $update['number'],
+          $dependency_map[$function],
+        ]);
+      }
+    }
+
+    $post_updates = \Drupal::service('update.post_update_registry')->getPendingUpdateFunctions();
+    if ($post_updates) {
+      // Now we rebuild all caches and after that execute hook_post_update().
+      $logger->info('Starting cache clear pre-step of database update.');
+      automatic_updates_console_command('cache:rebuild');
+      $logger->info('Finished cache clear pre-step of database update.');
+      foreach ($post_updates as $function) {
+        $this->executeDatabaseUpdate('update_invoke_post_update', [$function]);
+      }
+    }
+  }
+
+  /**
+   * Execute a single database update.
+   *
+   * @param callable $invoker
+   *   Callable update invoker.
+   * @param array $args
+   *   The arguments to pass to the invoker.
+   */
+  protected function executeDatabaseUpdate(callable $invoker, array $args) {
+    \Drupal::logger('automatic_updates')->notice('Database update running with arguments "@arguments"', ['@arguments' => print_r($args, TRUE)]);
+    $context = [
+      'sandbox'  => [],
+    ];
+    call_user_func_array($invoker, array_merge($args, [&$context]));
+    \Drupal::logger('automatic_updates')->notice('Database update finished with arguments "@arguments"', ['@arguments' => print_r($args, TRUE)]);
+  }
+
+}
diff --git a/src/Command/DatabaseUpdateStatus.php b/src/Command/DatabaseUpdateStatus.php
new file mode 100644
index 0000000000000000000000000000000000000000..b8e1085a82eb0e447741adf30e16af341dd1ec0a
--- /dev/null
+++ b/src/Command/DatabaseUpdateStatus.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\automatic_updates\Command;
+
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Database update status command.
+ */
+class DatabaseUpdateStatus extends BaseCommand {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function configure() {
+    parent::configure();
+    $this->setName('updatedb-status')
+      ->setDescription('Apply any database updates required (as with running update.php).')
+      ->setAliases(['updbst', 'updatedb:status']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function execute(InputInterface $input, OutputInterface $output) {
+    parent::execute($input, $output);
+    $pending_updates = \Drupal::service('automatic_updates.pending_db_updates')
+      ->run();
+    $output->writeln($pending_updates);
+  }
+
+}
diff --git a/src/DatabaseUpdateHandlerInterface.php b/src/DatabaseUpdateHandlerInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..0d14e3374c48d3dc5fed81459ee50b393eb68bc4
--- /dev/null
+++ b/src/DatabaseUpdateHandlerInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\automatic_updates;
+
+/**
+ * Interface for database_update_handler plugins.
+ */
+interface DatabaseUpdateHandlerInterface {
+
+  /**
+   * Returns the translated plugin label.
+   *
+   * @return string
+   *   The translated title.
+   */
+  public function label();
+
+  /**
+   * Handle database updates.
+   *
+   * @return bool
+   *   TRUE if database update was handled successfully, FALSE otherwise.
+   */
+  public function execute();
+
+}
diff --git a/src/DatabaseUpdateHandlerPluginBase.php b/src/DatabaseUpdateHandlerPluginBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..0aef167b5c20dc8b0198ddb781a82ea4163f39a7
--- /dev/null
+++ b/src/DatabaseUpdateHandlerPluginBase.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Drupal\automatic_updates;
+
+use Drupal\Component\Plugin\PluginBase;
+
+/**
+ * Base class for database_update_handler plugins.
+ */
+abstract class DatabaseUpdateHandlerPluginBase extends PluginBase implements DatabaseUpdateHandlerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function label() {
+    return $this->pluginDefinition['label'];
+  }
+
+}
diff --git a/src/DatabaseUpdateHandlerPluginManager.php b/src/DatabaseUpdateHandlerPluginManager.php
new file mode 100644
index 0000000000000000000000000000000000000000..5fc1da1facda7dc9d6397009965e28929d5d5b88
--- /dev/null
+++ b/src/DatabaseUpdateHandlerPluginManager.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\automatic_updates;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+
+/**
+ * DatabaseUpdateHandler plugin manager.
+ */
+class DatabaseUpdateHandlerPluginManager extends DefaultPluginManager {
+
+  /**
+   * Constructs DatabaseUpdateHandlerPluginManager object.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler to invoke the alter hook with.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct(
+      'Plugin/DatabaseUpdateHandler',
+      $namespaces,
+      $module_handler,
+      'Drupal\automatic_updates\DatabaseUpdateHandlerInterface',
+      'Drupal\automatic_updates\Annotation\DatabaseUpdateHandler'
+    );
+    $this->alterInfo('database_update_handler_info');
+    $this->setCacheBackend($cache_backend, 'database_update_handler_plugins');
+  }
+
+}
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index b18a9d404200c471afd79474005ad1a907b6d111..025d52c5e1a3cb7a7205cb5e74d75461303c0e27 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -40,6 +40,13 @@ class SettingsForm extends ConfigFormBase {
    */
   protected $updateManager;
 
+  /**
+   * The update processor.
+   *
+   * @var \Drupal\update\UpdateProcessorInterface
+   */
+  protected $updateProcessor;
+
   /**
    * {@inheritdoc}
    */
@@ -49,6 +56,7 @@ class SettingsForm extends ConfigFormBase {
     $instance->dateFormatter = $container->get('date.formatter');
     $instance->drupalRoot = (string) $container->get('app.root');
     $instance->updateManager = $container->get('update.manager');
+    $instance->updateProcessor = $container->get('update.processor');
     return $instance;
   }
 
@@ -126,6 +134,7 @@ class SettingsForm extends ConfigFormBase {
     ];
 
     $this->updateManager->refreshUpdateData();
+    $this->updateProcessor->fetchData();
     $available = update_get_available(TRUE);
     $projects = update_calculate_project_data($available);
     $not_recommended_version = $projects['drupal']['status'] !== UpdateManagerInterface::CURRENT;
diff --git a/src/Plugin/DatabaseUpdateHandler/ExecuteUpdates.php b/src/Plugin/DatabaseUpdateHandler/ExecuteUpdates.php
new file mode 100644
index 0000000000000000000000000000000000000000..8ce8de571f67517ec0884beb6ccd098cba8ce755
--- /dev/null
+++ b/src/Plugin/DatabaseUpdateHandler/ExecuteUpdates.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\automatic_updates\Plugin\DatabaseUpdateHandler;
+
+use Drupal\automatic_updates\DatabaseUpdateHandlerPluginBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Execute database updates.
+ *
+ * @DatabaseUpdateHandler(
+ *   id = "execute_updates",
+ *   label = "Execute database updates",
+ * )
+ */
+class ExecuteUpdates extends DatabaseUpdateHandlerPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The logger.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a new maintenance mode service.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The logger.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, LoggerInterface $logger) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->logger = $logger;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('logger.channel.automatic_updates')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    $process = automatic_updates_console_command('updatedb');
+    if ($errors = $process->getErrorOutput()) {
+      $this->logger->error($errors);
+      return FALSE;
+    }
+    return TRUE;
+  }
+
+}
diff --git a/src/Plugin/DatabaseUpdateHandler/IgnoreUpdates.php b/src/Plugin/DatabaseUpdateHandler/IgnoreUpdates.php
new file mode 100644
index 0000000000000000000000000000000000000000..3a8325b678113e42d015c72384c6d7f3638fab59
--- /dev/null
+++ b/src/Plugin/DatabaseUpdateHandler/IgnoreUpdates.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\automatic_updates\Plugin\DatabaseUpdateHandler;
+
+use Drupal\automatic_updates\DatabaseUpdateHandlerPluginBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Ignore database updates.
+ *
+ * @DatabaseUpdateHandler(
+ *   id = "ignore_updates",
+ *   label = "Ignore database updates",
+ * )
+ */
+class IgnoreUpdates extends DatabaseUpdateHandlerPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The logger.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a new maintenance mode service.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The logger.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, LoggerInterface $logger) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->logger = $logger;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('logger.channel.automatic_updates')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    $this->logger->notice('Database updates ignored.');
+    // Ignore the updates and hope for the best.
+    return TRUE;
+  }
+
+}
diff --git a/src/Plugin/DatabaseUpdateHandler/MaintenanceModeActivate.php b/src/Plugin/DatabaseUpdateHandler/MaintenanceModeActivate.php
new file mode 100644
index 0000000000000000000000000000000000000000..663aafc8a3decea0faf6df40e1ebac94b58b636d
--- /dev/null
+++ b/src/Plugin/DatabaseUpdateHandler/MaintenanceModeActivate.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\automatic_updates\Plugin\DatabaseUpdateHandler;
+
+use Drupal\automatic_updates\DatabaseUpdateHandlerPluginBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\State\StateInterface;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Put site into maintenance mode if there are database updates.
+ *
+ * @DatabaseUpdateHandler(
+ *   id = "maintenance_mode_activate",
+ *   label = "Put site into maintenance mode",
+ * )
+ */
+class MaintenanceModeActivate extends DatabaseUpdateHandlerPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The state.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * The logger.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a new maintenance mode service.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The logger.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, StateInterface $state, LoggerInterface $logger) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->state = $state;
+    $this->logger = $logger;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('state'),
+      $container->get('logger.channel.automatic_updates')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    $this->logger->notice('Maintenance mode activated.');
+    $this->state->set('system.maintenance_mode', TRUE);
+    return TRUE;
+  }
+
+}
diff --git a/src/Plugin/DatabaseUpdateHandler/MaintenanceModeDisactivate.php b/src/Plugin/DatabaseUpdateHandler/MaintenanceModeDisactivate.php
new file mode 100644
index 0000000000000000000000000000000000000000..212f71b03412d345a4c4ca10399fe3e253b6567d
--- /dev/null
+++ b/src/Plugin/DatabaseUpdateHandler/MaintenanceModeDisactivate.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\automatic_updates\Plugin\DatabaseUpdateHandler;
+
+use Drupal\automatic_updates\DatabaseUpdateHandlerPluginBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\State\StateInterface;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Remove site from maintenance mode.
+ *
+ * @DatabaseUpdateHandler(
+ *   id = "maintenance_mode_disactivate",
+ *   label = "Remove site from maintenance mode",
+ * )
+ */
+class MaintenanceModeDisactivate extends DatabaseUpdateHandlerPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The state.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * The logger.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a new maintenance mode service.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The logger.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, StateInterface $state, LoggerInterface $logger) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->state = $state;
+    $this->logger = $logger;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('state'),
+      $container->get('logger.channel.automatic_updates')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    $this->logger->notice('Maintenance mode dis-activated.');
+    $this->state->set('system.maintenance_mode', FALSE);
+    return TRUE;
+  }
+
+}
diff --git a/src/Plugin/DatabaseUpdateHandler/RollbackUpdate.php b/src/Plugin/DatabaseUpdateHandler/RollbackUpdate.php
new file mode 100644
index 0000000000000000000000000000000000000000..b20ec368218fcec31d88ef6ff841941b16eb1177
--- /dev/null
+++ b/src/Plugin/DatabaseUpdateHandler/RollbackUpdate.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\automatic_updates\Plugin\DatabaseUpdateHandler;
+
+use Drupal\automatic_updates\DatabaseUpdateHandlerPluginBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Rollback database updates.
+ *
+ * @DatabaseUpdateHandler(
+ *   id = "rollback",
+ *   label = "Rollback database updates",
+ * )
+ */
+class RollbackUpdate extends DatabaseUpdateHandlerPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The logger.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a new maintenance mode service.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The logger.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, LoggerInterface $logger) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->logger = $logger;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('logger.channel.automatic_updates')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    $this->logger->notice('Rollback initiated due to database updates.');
+    // Simply rollback the update by returning FALSE.
+    return FALSE;
+  }
+
+}
diff --git a/src/ReadinessChecker/MinimumPhpVersion.php b/src/ReadinessChecker/MinimumPhpVersion.php
index e9edc6fdf8ac3f32ce00d849e21f37e2d2d5903f..29f42c1e236ff45b55ccf0df8727674926208124 100644
--- a/src/ReadinessChecker/MinimumPhpVersion.php
+++ b/src/ReadinessChecker/MinimumPhpVersion.php
@@ -16,6 +16,8 @@ class MinimumPhpVersion extends SupportedPhpVersion {
    */
   protected function getUnsupportedVersionConstraint() {
     $parser = new VersionParser();
+    // Constant was introduced in 8.7.0-beta1, back fill for full 8.7 support.
+    defined('DRUPAL_MINIMUM_SUPPORTED_PHP') or define('DRUPAL_MINIMUM_SUPPORTED_PHP', '7.0.8');
     return $parser->parseConstraints('<' . DRUPAL_MINIMUM_SUPPORTED_PHP);
   }
 
diff --git a/src/ReadinessChecker/PendingDbUpdates.php b/src/ReadinessChecker/PendingDbUpdates.php
index 4cd4c6676f051ececc23bf53e1e0285eb360157e..e7a311b8c09f79b6a077bd405b414af3c71145f0 100644
--- a/src/ReadinessChecker/PendingDbUpdates.php
+++ b/src/ReadinessChecker/PendingDbUpdates.php
@@ -3,6 +3,7 @@
 namespace Drupal\automatic_updates\ReadinessChecker;
 
 use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Update\UpdateRegistry;
 
 /**
  * Pending database updates checker.
@@ -10,6 +11,23 @@ use Drupal\Core\StringTranslation\StringTranslationTrait;
 class PendingDbUpdates implements ReadinessCheckerInterface {
   use StringTranslationTrait;
 
+  /**
+   * The update registry.
+   *
+   * @var \Drupal\Core\Update\UpdateRegistry
+   */
+  protected $updateRegistry;
+
+  /**
+   * PendingDbUpdates constructor.
+   *
+   * @param \Drupal\Core\Update\UpdateRegistry $update_registry
+   *   The update registry.
+   */
+  public function __construct(UpdateRegistry $update_registry) {
+    $this->updateRegistry = $update_registry;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -17,7 +35,7 @@ class PendingDbUpdates implements ReadinessCheckerInterface {
     $messages = [];
 
     if ($this->areDbUpdatesPending()) {
-      $messages[] = $this->t('There are pending database updates, therefore updates cannot be applied. Please run update.php.');
+      $messages[] = $this->t('There are pending database updates. Please run update.php.');
     }
     return $messages;
   }
@@ -32,7 +50,9 @@ class PendingDbUpdates implements ReadinessCheckerInterface {
     require_once DRUPAL_ROOT . '/core/includes/install.inc';
     require_once DRUPAL_ROOT . '/core/includes/update.inc';
     drupal_load_updates();
-    return (bool) update_get_update_list();
+    $hook_updates = update_get_update_list();
+    $post_updates = $this->updateRegistry->getPendingUpdateFunctions();
+    return (bool) array_merge($hook_updates, $post_updates);
   }
 
 }
diff --git a/src/ReadinessChecker/ReadinessCheckerManager.php b/src/ReadinessChecker/ReadinessCheckerManager.php
index 0ad6dd6baf0489300ebc7239bb23151fb826f6be..eb1071f9cdb0b6cb1eafc090ca15d338b17f858f 100644
--- a/src/ReadinessChecker/ReadinessCheckerManager.php
+++ b/src/ReadinessChecker/ReadinessCheckerManager.php
@@ -25,12 +25,12 @@ class ReadinessCheckerManager implements ReadinessCheckerManagerInterface {
   protected $configFactory;
 
   /**
-   * An unsorted array of arrays of active checkers.
+   * An unsorted array of active checkers.
    *
-   * An associative array. The keys are integers that indicate priority. Values
-   * are arrays of ReadinessCheckerInterface objects.
+   * The keys are category, next level is integers that indicate priority.
+   * Values are arrays of ReadinessCheckerInterface objects.
    *
-   * @var \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerInterface[][]
+   * @var \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerInterface[][][]
    */
   protected $checkers = [];
 
diff --git a/src/Services/InPlaceUpdate.php b/src/Services/InPlaceUpdate.php
index fdd6f6fbb922d5507d5d90e4fb63078f30c92ef7..3bb44a114a6dbce3962c6e822cb821a1303604cd 100644
--- a/src/Services/InPlaceUpdate.php
+++ b/src/Services/InPlaceUpdate.php
@@ -145,12 +145,26 @@ class InPlaceUpdate implements UpdateInterface {
     if ($archive = $this->getArchive($project_name, $from_version, $to_version)) {
       $modified = $this->checkModifiedFiles($project_name, $project_type, $archive);
       if (!$modified && $this->backup($archive, $project_root)) {
+        $this->logger->info('In place update has started.');
         $success = $this->processUpdate($archive, $project_root);
+        $this->logger->info('In place update has finished.');
+        if ($success) {
+          $process = automatic_updates_console_command('updatedb:status');
+          if ($success && $process->getOutput()) {
+            $this->logger->info('Database update handling has started.');
+            $success = $this->handleDatabaseUpdates();
+            $this->logger->info('Database update handling has finished.');
+          }
+        }
         if (!$success) {
+          $this->logger->info('Rollback has started.');
           $this->rollback($project_root);
+          $this->logger->info('Rollback has finished.');
         }
-        else {
-          $this->clearOpcodeCache();
+        if ($success) {
+          $this->logger->info('Cache clear has started.');
+          $this->cacheRebuild();
+          $this->logger->info('Cache clear has finished.');
         }
       }
     }
@@ -319,6 +333,8 @@ class InPlaceUpdate implements UpdateInterface {
    *   The location of the downloaded archive.
    * @param string $csig
    *   The CSIG contents.
+   *
+   * @throws \SodiumException
    */
   protected function validateArchive($directory, $csig) {
     $module_path = drupal_get_path('module', 'automatic_updates');
@@ -345,7 +361,7 @@ class InPlaceUpdate implements UpdateInterface {
    */
   protected function backup(ArchiverInterface $archive, $project_root) {
     $backup = $this->fileSystem->createFilename('automatic_updates-backup', 'temporary://');
-    $this->fileSystem->prepareDirectory($backup);
+    $this->fileSystem->prepareDirectory($backup, FileSystemInterface::CREATE_DIRECTORY);
     $this->backup = $this->fileSystem->realpath($backup) . DIRECTORY_SEPARATOR;
     if (!$this->backup) {
       return FALSE;
@@ -426,17 +442,41 @@ class InPlaceUpdate implements UpdateInterface {
     if (!$this->backup) {
       return;
     }
-    foreach ($this->getFilesList($this->backup) as $file) {
+    foreach ($this->getFilesList($this->getTempDirectory()) as $file) {
       $file_real_path = $this->getFileRealPath($file);
-      $file_path = substr($file_real_path, strlen($this->backup));
+      $file_path = substr($file_real_path, strlen($this->getTempDirectory() . self::ARCHIVE_DIRECTORY));
+      $project_real_path = $this->getProjectRealPath($file_path, $project_root);
       try {
-        $this->fileSystem->copy($file_real_path, $this->getProjectRealPath($file_path, $project_root), FileSystemInterface::EXISTS_REPLACE);
-        $this->logger->info('"@file" was restored due to failure(s) in applying update.', ['@file' => $file_path]);
+        $this->fileSystem->delete($project_real_path);
+        $this->logger->info('"@file" was successfully removed during rollback.', ['@file' => $project_real_path]);
       }
       catch (FileException $exception) {
-        $this->logger->error('@file was not rolled back successfully.', ['@file' => $file_real_path]);
+        $this->logger->error('"@file" failed removal on rollback.', ['@file' => $project_real_path]);
       }
     }
+    foreach ($this->getFilesList($this->backup) as $file) {
+      $this->doRestore($file, $project_root);
+    }
+  }
+
+  /**
+   * Do restore.
+   *
+   * @param \SplFileInfo $file
+   *   File to restore.
+   * @param string $project_root
+   *   The project root directory.
+   */
+  protected function doRestore(\SplFileInfo $file, $project_root) {
+    $file_real_path = $this->getFileRealPath($file);
+    $file_path = substr($file_real_path, strlen($this->backup));
+    try {
+      $this->fileSystem->copy($file_real_path, $this->getProjectRealPath($file_path, $project_root), FileSystemInterface::EXISTS_REPLACE);
+      $this->logger->info('"@file" was successfully restored.', ['@file' => $file_path]);
+    }
+    catch (FileException $exception) {
+      $this->logger->error('"@file" failed restoration during rollback.', ['@file' => $file_real_path]);
+    }
   }
 
   /**
@@ -573,12 +613,31 @@ class InPlaceUpdate implements UpdateInterface {
   }
 
   /**
-   * Clear Opcode cache on successful update.
+   * Clear cache on successful update.
    */
-  protected function clearOpcodeCache() {
+  protected function cacheRebuild() {
     if (function_exists('opcache_reset')) {
       opcache_reset();
     }
+    automatic_updates_console_command('cache:rebuild');
+  }
+
+  /**
+   * Handle database updates.
+   *
+   * @return bool
+   *   TRUE if database updates were handled successfully. FALSE otherwise.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   */
+  protected function handleDatabaseUpdates() {
+    $result = TRUE;
+    /** @var \Drupal\Component\Plugin\PluginManagerInterface $database_update_handler */
+    $database_update_handler = \Drupal::service('plugin.manager.database_update_handler');
+    foreach ($this->configFactory->get('automatic_updates.settings')->get('database_update_handling') as $plugin_id) {
+      $result = $result && $database_update_handler->createInstance($plugin_id)->execute();
+    }
+    return $result;
   }
 
 }
diff --git a/tests/src/Build/InPlaceUpdateTest.php b/tests/src/Build/InPlaceUpdateTest.php
index 59d0b2f9638d3d5b4ab6dfed327dbea1a15dc678..c50c019f054bd137448baee3d463aae6be19ae5c 100644
--- a/tests/src/Build/InPlaceUpdateTest.php
+++ b/tests/src/Build/InPlaceUpdateTest.php
@@ -7,6 +7,7 @@ use Drupal\Component\FileSystem\FileSystem as DrupalFilesystem;
 use Drupal\Tests\automatic_updates\Build\QuickStart\QuickStartTestBase;
 use Drupal\Tests\automatic_updates\Traits\InstallTestTrait;
 use GuzzleHttp\Client;
+use GuzzleHttp\Exception\RequestException;
 use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;
 use Symfony\Component\Finder\Finder;
 
@@ -49,35 +50,29 @@ class InPlaceUpdateTest extends QuickStartTestBase {
 
   /**
    * @covers ::update
-   * @dataProvider coreVersionsProvider
+   * @dataProvider coreVersionsSuccessProvider
    */
   public function testCoreUpdate($from_version, $to_version) {
     $this->installCore($from_version);
+    $this->assertCoreUpgradeSuccess($from_version, $to_version);
+  }
 
-    // Assert files slated for deletion still exist.
-    foreach ($this->getDeletions('drupal', $from_version, $to_version) as $deletion) {
-      $this->assertFileExists($this->getWorkspaceDirectory() . DIRECTORY_SEPARATOR . $deletion);
-    }
-
-    // Update the site.
-    $assert = $this->visit("/test_automatic_updates/in-place-update/drupal/core/$from_version/$to_version")
-      ->assertSession();
-    $assert->statusCodeEquals(200);
-    $this->assertDrupalVisit();
+  /**
+   * @covers ::update
+   */
+  public function testCoreRollbackUpdate() {
+    $from_version = '8.7.0';
+    $to_version = '8.8.0';
+    $this->installCore($from_version);
 
-    // Assert that the update worked.
-    $assert->pageTextContains('Update successful');
-    $finder = new Finder();
-    $finder->files()->in($this->getWorkspaceDirectory())->path('core/lib/Drupal.php');
-    $finder->contains("/const VERSION = '$to_version'/");
-    $this->assertTrue($finder->hasResults());
-    $this->visit('/admin/reports/status');
-    $assert->pageTextContains("Drupal Version $to_version");
+    // Configure module to have db updates cause a rollback.
+    $settings_php = $this->getWorkspaceDirectory() . '/sites/default/settings.php';
+    $fs = new SymfonyFilesystem();
+    $fs->chmod($this->getWorkspaceDirectory() . '/sites/default', 0755);
+    $fs->chmod($settings_php, 0640);
+    $fs->appendToFile($settings_php, PHP_EOL . '$config[\'automatic_updates.settings\'][\'database_update_handling\'] = [\'rollback\'];' . PHP_EOL);
 
-    // Assert files slated for deletion are now gone.
-    foreach ($this->getDeletions('drupal', $from_version, $to_version) as $deletion) {
-      $this->assertFileNotExists($this->getWorkspaceDirectory() . DIRECTORY_SEPARATOR . $deletion);
-    }
+    $this->assertCoreUpgradeFailed($from_version, $to_version);
   }
 
   /**
@@ -88,7 +83,7 @@ class InPlaceUpdateTest extends QuickStartTestBase {
     $this->markTestSkipped('Contrib updates are not currently supported');
     $this->copyCodebase();
     $fs = new SymfonyFilesystem();
-    $fs->chmod($this->getWorkspaceDirectory() . '/sites/default', 0700, 0000);
+    $fs->chmod($this->getWorkspaceDirectory() . '/sites/default', 0700);
     $this->executeCommand('COMPOSER_DISCARD_CHANGES=true composer install --no-dev --no-interaction');
     $this->assertErrorOutputContains('Generating autoload files');
     $this->installQuickStart('standard');
@@ -100,7 +95,7 @@ class InPlaceUpdateTest extends QuickStartTestBase {
     $finder = new Finder();
     $finder->files()->in($this->getWorkspaceDirectory())->path("{$project_type}s/contrib/$project/$project.info.yml");
     $finder->contains("/version: '$from_version'/");
-    $this->assertTrue($finder->hasResults());
+    $this->assertTrue($finder->hasResults(), "Expected version $from_version does not exist in {$this->getWorkspaceDirectory()}/core/lib/Drupal.php");
 
     // Assert files slated for deletion still exist.
     foreach ($this->getDeletions($project, $from_version, $to_version) as $deletion) {
@@ -109,8 +104,10 @@ class InPlaceUpdateTest extends QuickStartTestBase {
 
     // Currently, this test has to use extension_discovery_scan_tests so we can
     // install test modules.
-    $fs->chmod($this->getWorkspaceDirectory() . '/sites/default/settings.php', 0640, 0000);
-    file_put_contents($this->getWorkspaceDirectory() . '/sites/default/settings.php', '$settings[\'extension_discovery_scan_tests\'] = TRUE;' . PHP_EOL, FILE_APPEND);
+    $fs = new SymfonyFilesystem();
+    $settings_php = $this->getWorkspaceDirectory() . '/sites/default/settings.php';
+    $fs->chmod($settings_php, 0640);
+    $fs->appendToFile($settings_php, '$settings[\'extension_discovery_scan_tests\'] = TRUE;' . PHP_EOL);
 
     // Log in so that we can install projects.
     $this->formLogin($this->adminUsername, $this->adminPassword);
@@ -134,7 +131,7 @@ class InPlaceUpdateTest extends QuickStartTestBase {
     $finder = new Finder();
     $finder->files()->in($this->getWorkspaceDirectory())->path("{$project_type}s/contrib/$project/$project.info.yml");
     $finder->contains("/version: '$to_version'/");
-    $this->assertTrue($finder->hasResults());
+    $this->assertTrue($finder->hasResults(), "Expected version $to_version does not exist in {$this->getWorkspaceDirectory()}/core/lib/Drupal.php");
     $this->assertDrupalVisit();
 
     // Assert files slated for deletion are now gone.
@@ -152,7 +149,7 @@ class InPlaceUpdateTest extends QuickStartTestBase {
   public function testCronCoreUpdate() {
     $this->installCore('8.7.6');
     $filesystem = new SymfonyFilesystem();
-    $filesystem->chmod($this->getWorkspaceDirectory() . '/sites/default', 0750, 0000);
+    $filesystem->chmod($this->getWorkspaceDirectory() . '/sites/default', 0750);
     $settings_php = $this->getWorkspaceDirectory() . '/sites/default/settings.php';
     $filesystem->chmod($settings_php, 0640);
     $filesystem->appendToFile($settings_php, PHP_EOL . '$config[\'automatic_updates.settings\'][\'enable_cron_updates\'] = TRUE;' . PHP_EOL);
@@ -166,7 +163,7 @@ class InPlaceUpdateTest extends QuickStartTestBase {
     $finder->files()->in($this->getWorkspaceDirectory())->path('core/lib/Drupal.php');
     $finder->notContains("/const VERSION = '8.7.6'/");
     $finder->contains("/const VERSION = '8.7./");
-    $this->assertTrue($finder->hasResults());
+    $this->assertTrue($finder->hasResults(), "Expected version 8.7.{x} does not exist in {$this->getWorkspaceDirectory()}/core/lib/Drupal.php");
   }
 
   /**
@@ -187,15 +184,17 @@ class InPlaceUpdateTest extends QuickStartTestBase {
     $this->executeCommand('git reset HEAD --hard');
     $this->assertCommandSuccessful();
     $fs = new SymfonyFilesystem();
-    $fs->chmod($this->getWorkspaceDirectory() . '/sites/default', 0700, 0000);
+    $fs->chmod($this->getWorkspaceDirectory() . '/sites/default', 0700);
     $this->executeCommand('COMPOSER_DISCARD_CHANGES=true composer install --no-dev --no-interaction');
     $this->assertErrorOutputContains('Generating autoload files');
     $this->installQuickStart('minimal');
 
     // Currently, this test has to use extension_discovery_scan_tests so we can
     // install test modules.
-    $fs->chmod($this->getWorkspaceDirectory() . '/sites/default/settings.php', 0640, 0000);
-    file_put_contents($this->getWorkspaceDirectory() . '/sites/default/settings.php', '$settings[\'extension_discovery_scan_tests\'] = TRUE;' . PHP_EOL, FILE_APPEND);
+    $settings_php = $this->getWorkspaceDirectory() . '/sites/default/settings.php';
+    $fs->chmod($this->getWorkspaceDirectory() . '/sites/default', 0755);
+    $fs->chmod($settings_php, 0640);
+    $fs->appendToFile($settings_php, '$settings[\'extension_discovery_scan_tests\'] = TRUE;' . PHP_EOL);
 
     // Log in so that we can install modules.
     $this->formLogin($this->adminUsername, $this->adminPassword);
@@ -207,7 +206,7 @@ class InPlaceUpdateTest extends QuickStartTestBase {
     $finder = new Finder();
     $finder->files()->in($this->getWorkspaceDirectory())->path('core/lib/Drupal.php');
     $finder->contains("/const VERSION = '$starting_version'/");
-    $this->assertTrue($finder->hasResults());
+    $this->assertTrue($finder->hasResults(), "Expected version $starting_version does not exist in {$this->getWorkspaceDirectory()}/core/lib/Drupal.php");
 
     // Assert that the site is functional after install.
     $this->visit();
@@ -215,13 +214,21 @@ class InPlaceUpdateTest extends QuickStartTestBase {
   }
 
   /**
-   * Core versions data provider.
+   * Core versions data provider resulting in a successful upgrade.
    */
-  public function coreVersionsProvider() {
+  public function coreVersionsSuccessProvider() {
+    $datum[] = [
+      'from' => '8.7.2',
+      'to' => '8.7.4',
+    ];
     $datum[] = [
       'from' => '8.7.0',
       'to' => '8.7.1',
     ];
+    $datum[] = [
+      'from' => '8.7.2',
+      'to' => '8.7.10',
+    ];
     $datum[] = [
       'from' => '8.7.6',
       'to' => '8.7.7',
@@ -256,13 +263,12 @@ class InPlaceUpdateTest extends QuickStartTestBase {
       return $this->deletions;
     }
     $this->deletions = [];
-    $http_client = new Client();
     $filesystem = new SymfonyFilesystem();
     $this->deletionsDestination = DrupalFileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR . "$project-" . mt_rand(10000, 99999) . microtime(TRUE);
     $filesystem->mkdir($this->deletionsDestination);
     $file_name = "$project-$from_version-to-$to_version.zip";
     $zip_file = $this->deletionsDestination . DIRECTORY_SEPARATOR . $file_name;
-    $http_client->get("https://www.drupal.org/in-place-updates/$project/$file_name", ['sink' => $zip_file]);
+    $this->doGetArchive($project, $file_name, $zip_file);
     $zip = new \ZipArchive();
     $zip->open($zip_file);
     $zip->extractTo($this->deletionsDestination, [InPlaceUpdate::DELETION_MANIFEST]);
@@ -278,4 +284,111 @@ class InPlaceUpdateTest extends QuickStartTestBase {
     return $this->deletions;
   }
 
+  /**
+   * Get the archive with protection against 429s.
+   *
+   * @param string $project
+   *   The project.
+   * @param string $file_name
+   *   The filename.
+   * @param string $zip_file
+   *   The zip file path.
+   * @param int $delay
+   *   (optional) The delay.
+   */
+  protected function doGetArchive($project, $file_name, $zip_file, $delay = 0) {
+    try {
+      sleep($delay);
+      $http_client = new Client();
+      $http_client->get("https://www.drupal.org/in-place-updates/$project/$file_name", ['sink' => $zip_file]);
+    }
+    catch (RequestException $exception) {
+      $response = $exception->getResponse();
+      if ($response && $response->getStatusCode() === 429) {
+        $this->doGetArchive($project, $file_name, $zip_file, 10);
+      }
+      else {
+        throw $exception;
+      }
+    }
+  }
+
+  /**
+   * Assert an upgrade succeeded.
+   *
+   * @param string $from_version
+   *   The version from which to upgrade.
+   * @param string $to_version
+   *   The version to which to upgrade.
+   *
+   * @throws \Behat\Mink\Exception\ExpectationException
+   * @throws \Behat\Mink\Exception\ResponseTextException
+   */
+  public function assertCoreUpgradeSuccess($from_version, $to_version) {
+    // Assert files slated for deletion still exist.
+    foreach ($this->getDeletions('drupal', $from_version, $to_version) as $deletion) {
+      $this->assertFileExists($this->getWorkspaceDirectory() . DIRECTORY_SEPARATOR . $deletion);
+    }
+
+    // Update the site.
+    $assert = $this->visit("/test_automatic_updates/in-place-update/drupal/core/$from_version/$to_version")
+      ->assertSession();
+    $assert->statusCodeEquals(200);
+    $this->assertDrupalVisit();
+
+    // Assert that the update worked.
+    $finder = new Finder();
+    $finder->files()->in($this->getWorkspaceDirectory())->path('core/lib/Drupal.php');
+    $finder->contains("/const VERSION = '$to_version'/");
+    $this->assertTrue($finder->hasResults(), "Expected version $to_version does not exist in {$this->getWorkspaceDirectory()}/core/lib/Drupal.php");
+    $assert->pageTextContains('Update successful');
+    $this->visit('/admin/reports/status');
+    $assert->pageTextContains("Drupal Version $to_version");
+
+    // Assert files slated for deletion are now gone.
+    foreach ($this->getDeletions('drupal', $from_version, $to_version) as $deletion) {
+      $this->assertFileNotExists($this->getWorkspaceDirectory() . DIRECTORY_SEPARATOR . $deletion);
+    }
+
+    // Validate that all DB updates are processed.
+    $this->visit('/update.php/selection');
+    $assert->pageTextContains('No pending updates.');
+  }
+
+  /**
+   * Assert an upgraded failed and was handle appropriately.
+   *
+   * @param string $from_version
+   *   The version from which to upgrade.
+   * @param string $to_version
+   *   The version to which to upgrade.
+   *
+   * @throws \Behat\Mink\Exception\ResponseTextException
+   */
+  public function assertCoreUpgradeFailed($from_version, $to_version) {
+    // Assert files slated for deletion still exist.
+    foreach ($this->getDeletions('drupal', $from_version, $to_version) as $deletion) {
+      $this->assertFileExists($this->getWorkspaceDirectory() . DIRECTORY_SEPARATOR . $deletion);
+    }
+
+    // Update the site.
+    $assert = $this->visit("/test_automatic_updates/in-place-update/drupal/core/$from_version/$to_version")
+      ->assertSession();
+    $assert->statusCodeEquals(200);
+
+    // Assert that the update failed.
+    $finder = new Finder();
+    $finder->files()->in($this->getWorkspaceDirectory())->path('core/lib/Drupal.php');
+    $finder->contains("/const VERSION = '$from_version'/");
+    $this->assertTrue($finder->hasResults(), "Expected version $from_version does not exist in {$this->getWorkspaceDirectory()}/core/lib/Drupal.php");
+    $assert->pageTextContains('Update Failed');
+    $this->visit('/admin/reports/status');
+    $assert->pageTextContains("Drupal Version $from_version");
+
+    // Assert files slated for deletion are restored.
+    foreach ($this->getDeletions('drupal', $from_version, $to_version) as $deletion) {
+      $this->assertFileExists($this->getWorkspaceDirectory() . DIRECTORY_SEPARATOR . $deletion);
+    }
+  }
+
 }
diff --git a/tests/src/Build/ModifiedFilesTest.php b/tests/src/Build/ModifiedFilesTest.php
index 57a5f8073c2c0b8d1ed2e1948534c5bc43440a32..76ce2874dbb1aff9dc774bdf2b2de63133cb4130 100644
--- a/tests/src/Build/ModifiedFilesTest.php
+++ b/tests/src/Build/ModifiedFilesTest.php
@@ -44,7 +44,7 @@ class ModifiedFilesTest extends QuickStartTestBase {
     // We have to fetch the tags for this shallow repo. It might not be a
     // shallow clone, therefore we use executeCommand instead of assertCommand.
     $this->executeCommand('git fetch --unshallow  --tags');
-    $this->symfonyFileSystem->chmod($this->getWorkspaceDirectory() . '/sites/default', 0700, 0000);
+    $this->symfonyFileSystem->chmod($this->getWorkspaceDirectory() . '/sites/default', 0700);
     $this->executeCommand('git reset HEAD --hard');
     $this->assertCommandSuccessful();
     $this->executeCommand("git checkout $version -f");
@@ -118,14 +118,14 @@ class ModifiedFilesTest extends QuickStartTestBase {
    *   The modified files to assert.
    */
   protected function assertModifications($project_type, $project, array $modifications) {
-    $this->symfonyFileSystem->chmod($this->getWorkspaceDirectory() . '/sites/default', 0700, 0000);
+    $this->symfonyFileSystem->chmod($this->getWorkspaceDirectory() . '/sites/default', 0700);
     $this->executeCommand('COMPOSER_DISCARD_CHANGES=true composer install --no-dev --no-interaction');
     $this->assertErrorOutputContains('Generating autoload files');
     $this->installQuickStart('minimal');
 
     // Currently, this test has to use extension_discovery_scan_tests so we can
     // install test modules.
-    $this->symfonyFileSystem->chmod($this->getWorkspaceDirectory() . '/sites/default', 0750, 0000);
+    $this->symfonyFileSystem->chmod($this->getWorkspaceDirectory() . '/sites/default', 0750);
     $settings_php = $this->getWorkspaceDirectory() . '/sites/default/settings.php';
     $this->symfonyFileSystem->chmod($settings_php, 0640);
     $this->symfonyFileSystem->appendToFile($settings_php, PHP_EOL . '$settings[\'extension_discovery_scan_tests\'] = TRUE;' . PHP_EOL);
diff --git a/tests/src/Kernel/ReadinessChecker/PendingDbUpdatesTest.php b/tests/src/Kernel/ReadinessChecker/PendingDbUpdatesTest.php
index 2a93b5e6415ccf20fb782e757722b146ec6470ec..6a693790a6aadda762ad16d8de4516b25a86856e 100644
--- a/tests/src/Kernel/ReadinessChecker/PendingDbUpdatesTest.php
+++ b/tests/src/Kernel/ReadinessChecker/PendingDbUpdatesTest.php
@@ -27,7 +27,7 @@ class PendingDbUpdatesTest extends KernelTestBase {
     $this->assertEmpty($messages);
 
     $messages = (new TestPendingDbUpdates())->run();
-    self::assertEquals('There are pending database updates, therefore updates cannot be applied. Please run update.php.', $messages[0]);
+    self::assertEquals('There are pending database updates. Please run update.php.', $messages[0]);
   }
 
 }
@@ -37,6 +37,11 @@ class PendingDbUpdatesTest extends KernelTestBase {
  */
 class TestPendingDbUpdates extends PendingDbUpdates {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct() {}
+
   /**
    * {@inheritdoc}
    */