diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index 5d05cd5..9d648a7 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -15,10 +15,8 @@
  */
 
 use Drupal\Component\Utility\Timer;
-use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Batch\BatchQueueController;
 use Drupal\Core\Batch\Percentage;
-use Drupal\Core\Form\FormState;
-use Drupal\Core\Url;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -205,164 +203,22 @@ function _batch_progress_page() {
  *
  * @return array
  *   An array containing a completion value (in percent) and a status message.
- */
-function _batch_process() {
-  $batch       = &batch_get();
-  $current_set = &_batch_current_set();
-  // Indicate that this batch set needs to be initialized.
-  $set_changed = TRUE;
-
-  // If this batch was marked for progressive execution (e.g. forms submitted by
-  // \Drupal::formBuilder()->submitForm(), initialize a timer to determine
-  // whether we need to proceed with the same batch phase when a processing time
-  // of 1 second has been exceeded.
-  if ($batch['progressive']) {
-    Timer::start('batch_processing');
-  }
-
-  if (empty($current_set['start'])) {
-    $current_set['start'] = microtime(TRUE);
-  }
-
-  $queue = _batch_queue($current_set);
-
-  while (!$current_set['success']) {
-    // If this is the first time we iterate this batch set in the current
-    // request, we check if it requires an additional file for functions
-    // definitions.
-    if ($set_changed && isset($current_set['file']) && is_file($current_set['file'])) {
-      include_once \Drupal::root() . '/' . $current_set['file'];
-    }
-
-    $task_message = $label = '';
-    // Assume a single pass operation and set the completion level to 1 by
-    // default.
-    $finished = 1;
-
-    if ($item = $queue->claimItem()) {
-      list($callback, $args) = $item->data;
-
-      // Build the 'context' array and execute the function call.
-      $batch_context = [
-        'sandbox'  => &$current_set['sandbox'],
-        'results'  => &$current_set['results'],
-        'finished' => &$finished,
-        'message'  => &$task_message,
-      ];
-      call_user_func_array($callback, array_merge($args, [&$batch_context]));
-
-      if ($finished >= 1) {
-        // Make sure this step is not counted twice when computing $current.
-        $finished = 0;
-        // Remove the processed operation and clear the sandbox.
-        $queue->deleteItem($item);
-        $current_set['count']--;
-        $current_set['sandbox'] = [];
-      }
-    }
-
-    // When all operations in the current batch set are completed, browse
-    // through the remaining sets, marking them 'successfully processed'
-    // along the way, until we find a set that contains operations.
-    // _batch_next_set() executes form submit handlers stored in 'control'
-    // sets (see \Drupal::service('form_submitter')), which can in turn add new
-    // sets to the batch.
-    $set_changed = FALSE;
-    $old_set = $current_set;
-    while (empty($current_set['count']) && ($current_set['success'] = TRUE) && _batch_next_set()) {
-      $current_set = &_batch_current_set();
-      $current_set['start'] = microtime(TRUE);
-      $set_changed = TRUE;
-    }
-
-    // At this point, either $current_set contains operations that need to be
-    // processed or all sets have been completed.
-    $queue = _batch_queue($current_set);
-
-    // If we are in progressive mode, break processing after 1 second.
-    if ($batch['progressive'] && Timer::read('batch_processing') > 1000) {
-      // Record elapsed wall clock time.
-      $current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
-      break;
-    }
-  }
-
-  if ($batch['progressive']) {
-    // Gather progress information.
-
-    // Reporting 100% progress will cause the whole batch to be considered
-    // processed. If processing was paused right after moving to a new set,
-    // we have to use the info from the new (unprocessed) set.
-    if ($set_changed && isset($current_set['queue'])) {
-      // Processing will continue with a fresh batch set.
-      $remaining        = $current_set['count'];
-      $total            = $current_set['total'];
-      $progress_message = $current_set['init_message'];
-      $task_message     = '';
-    }
-    else {
-      // Processing will continue with the current batch set.
-      $remaining        = $old_set['count'];
-      $total            = $old_set['total'];
-      $progress_message = $old_set['progress_message'];
-    }
-
-    // Total progress is the number of operations that have fully run plus the
-    // completion level of the current operation.
-    $current    = $total - $remaining + $finished;
-    $percentage = _batch_api_percentage($total, $current);
-    $elapsed    = isset($current_set['elapsed']) ? $current_set['elapsed'] : 0;
-    $values     = [
-      '@remaining'  => $remaining,
-      '@total'      => $total,
-      '@current'    => floor($current),
-      '@percentage' => $percentage,
-      '@elapsed'    => \Drupal::service('date.formatter')->formatInterval($elapsed / 1000),
-      // If possible, estimate remaining processing time.
-      '@estimate'   => ($current > 0) ? \Drupal::service('date.formatter')->formatInterval(($elapsed * ($total - $current) / $current) / 1000) : '-',
-    ];
-    $message = strtr($progress_message, $values);
-    if (!empty($task_message)) {
-      $label = $task_message;
-    }
-
-    return [$percentage, $message, $label];
-  }
-  else {
-    // If we are not in progressive mode, the entire batch has been processed.
-    return _batch_finished();
-  }
-}
-
-/**
- * Formats the percent completion for a batch set.
  *
- * @param int $total
- *   The total number of operations.
- * @param int|float $current
- *   The number of the current operation. This may be a floating point number
- *   rather than an integer in the case of a multi-step operation that is not
- *   yet complete; in that case, the fractional part of $current represents the
- *   fraction of the operation that has been completed.
- *
- * @return string
- *   The properly formatted percentage, as a string. We output percentages
- *   using the correct number of decimal places so that we never print "100%"
- *   until we are finished, but we also never print more decimal places than
- *   are meaningful.
- *
- * @see _batch_process()
+ * @deprecated as of Drupal 8.2.x, will be removed in Drupal 9.0.0. Use
+ *   \Drupal\Core\Batch\BatchQueueController::processQueue() instead.
  */
-function _batch_api_percentage($total, $current) {
-  return Percentage::format($total, $current);
+function _batch_process() {
+  return BatchQueueController::processQueue();
 }
 
 /**
  * Returns the batch set being currently processed.
+ *
+ * @deprecated as of Drupal 8.2.x, will be removed in Drupal 9.0.0. Use
+ *   \Drupal\Core\Batch\BatchQueueController::getCurrentSet() instead.
  */
 function &_batch_current_set() {
-  $batch = &batch_get();
-  return $batch['sets'][$batch['current_set']];
+  return BatchQueueController::getCurrentSet();
 }
 
 /**
@@ -375,20 +231,12 @@ function &_batch_current_set() {
  * @return true|null
  *   TRUE if a subsequent set was found in the batch; no value will be returned
  *   if no subsequent set was found.
+ *
+ * @deprecated as of Drupal 8.2.x, will be removed in Drupal 9.0.0. Use
+ *   \Drupal\Core\Batch\BatchQueueController::hasNextSet() instead.
  */
 function _batch_next_set() {
-  $batch = &batch_get();
-  if (isset($batch['sets'][$batch['current_set'] + 1])) {
-    $batch['current_set']++;
-    $current_set = &_batch_current_set();
-    if (isset($current_set['form_submit']) && ($callback = $current_set['form_submit']) && is_callable($callback)) {
-      // We use our stored copies of $form and $form_state to account for
-      // possible alterations by previous form submit handlers.
-      $complete_form = &$batch['form_state']->getCompleteForm();
-      call_user_func_array($callback, [&$complete_form, &$batch['form_state']]);
-    }
-    return TRUE;
-  }
+  return BatchQueueController::nextSet();
 }
 
 /**
@@ -396,110 +244,12 @@ function _batch_next_set() {
  *
  * Call the 'finished' callback of each batch set to allow custom handling of
  * the results and resolve page redirection.
+ *
+ * @deprecated as of Drupal 8.2.x, will be removed in Drupal 9.0.0. Use
+ *   \Drupal\Core\Batch\BatchQueueController::finishedProcessing() instead.
  */
 function _batch_finished() {
-  $batch = &batch_get();
-  $batch_finished_redirect = NULL;
-
-  // Execute the 'finished' callbacks for each batch set, if defined.
-  foreach ($batch['sets'] as $batch_set) {
-    if (isset($batch_set['finished'])) {
-      // Check if the set requires an additional file for function definitions.
-      if (isset($batch_set['file']) && is_file($batch_set['file'])) {
-        include_once \Drupal::root() . '/' . $batch_set['file'];
-      }
-      if (is_callable($batch_set['finished'])) {
-        $queue = _batch_queue($batch_set);
-        $operations = $queue->getAllItems();
-        $batch_set_result = call_user_func_array($batch_set['finished'], [$batch_set['success'], $batch_set['results'], $operations, \Drupal::service('date.formatter')->formatInterval($batch_set['elapsed'] / 1000)]);
-        // If a batch 'finished' callback requested a redirect after the batch
-        // is complete, save that for later use. If more than one batch set
-        // returned a redirect, the last one is used.
-        if ($batch_set_result instanceof RedirectResponse) {
-          $batch_finished_redirect = $batch_set_result;
-        }
-      }
-    }
-  }
-
-  // Clean up the batch table and unset the static $batch variable.
-  if ($batch['progressive']) {
-    \Drupal::service('batch.storage')->delete($batch['id']);
-    foreach ($batch['sets'] as $batch_set) {
-      if ($queue = _batch_queue($batch_set)) {
-        $queue->deleteQueue();
-      }
-    }
-    // Clean-up the session. Not needed for CLI updates.
-    if (isset($_SESSION)) {
-      unset($_SESSION['batches'][$batch['id']]);
-      if (empty($_SESSION['batches'])) {
-        unset($_SESSION['batches']);
-      }
-    }
-  }
-  $_batch = $batch;
-  $batch = NULL;
-
-  // Redirect if needed.
-  if ($_batch['progressive']) {
-    // Revert the 'destination' that was saved in batch_process().
-    if (isset($_batch['destination'])) {
-      \Drupal::request()->query->set('destination', $_batch['destination']);
-    }
-
-    // Determine the target path to redirect to. If a batch 'finished' callback
-    // returned a redirect response object, use that. Otherwise, fall back on
-    // the form redirection.
-    if (isset($batch_finished_redirect)) {
-      return $batch_finished_redirect;
-    }
-    elseif (!isset($_batch['form_state'])) {
-      $_batch['form_state'] = new FormState();
-    }
-    if ($_batch['form_state']->getRedirect() === NULL) {
-      $redirect = $_batch['batch_redirect'] ?: $_batch['source_url'];
-      // Any path with a scheme does not correspond to a route.
-      if (!$redirect instanceof Url) {
-        $options = UrlHelper::parse($redirect);
-        if (parse_url($options['path'], PHP_URL_SCHEME)) {
-          $redirect = Url::fromUri($options['path'], $options);
-        }
-        else {
-          $redirect = \Drupal::pathValidator()->getUrlIfValid($options['path']);
-          if (!$redirect) {
-            // Stay on the same page if the redirect was invalid.
-            $redirect = Url::fromRoute('<current>');
-          }
-          $redirect->setOptions($options);
-        }
-      }
-      $_batch['form_state']->setRedirectUrl($redirect);
-    }
-
-    // Use \Drupal\Core\Form\FormSubmitterInterface::redirectForm() to handle
-    // the redirection logic.
-    $redirect = \Drupal::service('form_submitter')->redirectForm($_batch['form_state']);
-    if (is_object($redirect)) {
-      return $redirect;
-    }
-
-    // If no redirection happened, redirect to the originating page. In case the
-    // form needs to be rebuilt, save the final $form_state for
-    // \Drupal\Core\Form\FormBuilderInterface::buildForm().
-    if ($_batch['form_state']->isRebuilding()) {
-      $_SESSION['batch_form_state'] = $_batch['form_state'];
-    }
-    $callback = $_batch['redirect_callback'];
-    $_batch['source_url']->mergeOptions(['query' => ['op' => 'finish', 'id' => $_batch['id']]]);
-    if (is_callable($callback)) {
-      $callback($_batch['source_url'], $_batch['source_url']->getOption('query'));
-    }
-    elseif ($callback === NULL) {
-      // Default to RedirectResponse objects when nothing specified.
-      return new RedirectResponse($_batch['source_url']->setAbsolute()->toString());
-    }
-  }
+  return BatchQueueController::finishedProcessing();
 }
 
 /**
diff --git a/core/includes/form.inc b/core/includes/form.inc
index 05f2a9e..2dc94cb 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -6,11 +6,12 @@
  */
 
 use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Batch\Batch;
+use Drupal\Core\Batch\BatchQueueController;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Render\Element\RenderElement;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Url;
-use Symfony\Component\HttpFoundation\RedirectResponse;
 
 /**
  * Prepares variables for select element templates.
@@ -663,9 +664,10 @@ function template_preprocess_form_element_label(&$variables) {
  * 'finished' callback. Batch sets are processed sequentially, with the progress
  * bar starting afresh for each new set.
  *
- * @param $batch_definition
- *   An associative array defining the batch, with the following elements (all
- *   are optional except as noted):
+ * @param \Drupal\Core\Batch\Batch|array $batch_definition
+ *   An object representing the batch or an associative array defining the
+ *   batch. If an array is used then it has the following elements (all are
+ *   optional except as noted):
  *   - operations: (required) Array of operations to be performed, where each
  *     item is an array consisting of the name of an implementation of
  *     callback_batch_operation() and an array of parameter.
@@ -710,60 +712,15 @@ function template_preprocess_form_element_label(&$variables) {
  *       Typically, the class will either be \Drupal\Core\Queue\Batch or
  *       \Drupal\Core\Queue\BatchMemory. Defaults to Batch if progressive is
  *       TRUE, or to BatchMemory if progressive is FALSE.
+ *
+ * @deprecated as of Drupal 8.2.x, will be removed in Drupal 9.0.0. Create a
+ *   \Drupal\Core\Batch\Batch object and use the enqueue() function.
  */
 function batch_set($batch_definition) {
-  if ($batch_definition) {
-    $batch =& batch_get();
-
-    // Initialize the batch if needed.
-    if (empty($batch)) {
-      $batch = [
-        'sets' => [],
-        'has_form_submits' => FALSE,
-      ];
-    }
-
-    // Base and default properties for the batch set.
-    $init = [
-      'sandbox' => [],
-      'results' => [],
-      'success' => FALSE,
-      'start' => 0,
-      'elapsed' => 0,
-    ];
-    $defaults = [
-      'title' => t('Processing'),
-      'init_message' => t('Initializing.'),
-      'progress_message' => t('Completed @current of @total.'),
-      'error_message' => t('An error has occurred.'),
-    ];
-    $batch_set = $init + $batch_definition + $defaults;
-
-    // Tweak init_message to avoid the bottom of the page flickering down after
-    // init phase.
-    $batch_set['init_message'] .= '<br/>&nbsp;';
-
-    // The non-concurrent workflow of batch execution allows us to save
-    // numberOfItems() queries by handling our own counter.
-    $batch_set['total'] = count($batch_set['operations']);
-    $batch_set['count'] = $batch_set['total'];
-
-    // Add the set to the batch.
-    if (empty($batch['id'])) {
-      // The batch is not running yet. Simply add the new set.
-      $batch['sets'][] = $batch_set;
-    }
-    else {
-      // The set is being added while the batch is running. Insert the new set
-      // right after the current one to ensure execution order, and store its
-      // operations in a queue.
-      $index = $batch['current_set'] + 1;
-      $slice1 = array_slice($batch['sets'], 0, $index);
-      $slice2 = array_slice($batch['sets'], $index);
-      $batch['sets'] = array_merge($slice1, [$batch_set], $slice2);
-      _batch_populate_queue($batch, $index);
-    }
+  if (!($batch_definition instanceof Batch)) {
+    $batch_definition = Batch::createFromArray($batch_definition);
   }
+  $batch_definition->enqueue();
 }
 
 /**
@@ -786,94 +743,21 @@ function batch_set($batch_definition) {
  *   is omitted and no redirect response was returned by the 'finished'
  *   callback. Any query arguments will be automatically persisted.
  * @param \Drupal\Core\Url $url
- *   (optional - should only be used for separate scripts like update.php)
- *   URL of the batch processing page.
- * @param $redirect_callback
+ *   (optional) URL of the batch processing page.
+ *   Should only be used for separate scripts like update.php.
+ * @param string $redirect_callback
  *   (optional) Specify a function to be called to redirect to the progressive
  *   processing page.
  *
  * @return \Symfony\Component\HttpFoundation\RedirectResponse|null
  *   A redirect response if the batch is progressive. No return value otherwise.
+ *
+ * @deprecated as of Drupal 8.2.x, will be removed in Drupal 9.0.0. Use
+ *   \Drupal\Core\Batch\Batch::process() instead or the process() function on
+ *   an existing Batch object.
  */
 function batch_process($redirect = NULL, Url $url = NULL, $redirect_callback = NULL) {
-  $batch =& batch_get();
-
-  if (isset($batch)) {
-    // Add process information
-    $process_info = [
-      'current_set' => 0,
-      'progressive' => TRUE,
-      'url' => isset($url) ? $url : Url::fromRoute('system.batch_page.html'),
-      'source_url' => Url::fromRouteMatch(\Drupal::routeMatch())->mergeOptions(['query' => \Drupal::request()->query->all()]),
-      'batch_redirect' => $redirect,
-      'theme' => \Drupal::theme()->getActiveTheme()->getName(),
-      'redirect_callback' => $redirect_callback,
-    ];
-    $batch += $process_info;
-
-    // The batch is now completely built. Allow other modules to make changes
-    // to the batch so that it is easier to reuse batch processes in other
-    // environments.
-    \Drupal::moduleHandler()->alter('batch', $batch);
-
-    // Assign an arbitrary id: don't rely on a serial column in the 'batch'
-    // table, since non-progressive batches skip database storage completely.
-    $batch['id'] = db_next_id();
-
-    // Move operations to a job queue. Non-progressive batches will use a
-    // memory-based queue.
-    foreach ($batch['sets'] as $key => $batch_set) {
-      _batch_populate_queue($batch, $key);
-    }
-
-    // Initiate processing.
-    if ($batch['progressive']) {
-      // Now that we have a batch id, we can generate the redirection link in
-      // the generic error message.
-      /** @var \Drupal\Core\Url $batch_url */
-      $batch_url = $batch['url'];
-      /** @var \Drupal\Core\Url $error_url */
-      $error_url = clone $batch_url;
-      $query_options = $error_url->getOption('query');
-      $query_options['id'] = $batch['id'];
-      $query_options['op'] = 'finished';
-      $error_url->setOption('query', $query_options);
-
-      $batch['error_message'] = t('Please continue to <a href=":error_url">the error page</a>', [':error_url' => $error_url->toString(TRUE)->getGeneratedUrl()]);
-
-      // Clear the way for the redirection to the batch processing page, by
-      // saving and unsetting the 'destination', if there is any.
-      $request = \Drupal::request();
-      if ($request->query->has('destination')) {
-        $batch['destination'] = $request->query->get('destination');
-        $request->query->remove('destination');
-      }
-
-      // Store the batch.
-      \Drupal::service('batch.storage')->create($batch);
-
-      // Set the batch number in the session to guarantee that it will stay alive.
-      $_SESSION['batches'][$batch['id']] = TRUE;
-
-      // Redirect for processing.
-      $query_options = $error_url->getOption('query');
-      $query_options['op'] = 'start';
-      $query_options['id'] = $batch['id'];
-      $batch_url->setOption('query', $query_options);
-      if (($function = $batch['redirect_callback']) && function_exists($function)) {
-        $function($batch_url->toString(), ['query' => $query_options]);
-      }
-      else {
-        return new RedirectResponse($batch_url->setAbsolute()->toString(TRUE)->getGeneratedUrl());
-      }
-    }
-    else {
-      // Non-progressive execution: bypass the whole progressbar workflow
-      // and execute the batch in one pass.
-      require_once __DIR__ . '/batch.inc';
-      _batch_process();
-    }
-  }
+  return BatchQueueController::process($redirect, $url, $redirect_callback);
 }
 
 /**
@@ -886,69 +770,7 @@ function &batch_get() {
   // that are part of the Batch API and need to reset the batch information may
   // call batch_get() and manipulate the result by reference. Functions that are
   // not part of the Batch API can also do this, but shouldn't.
-  static $batch = [];
-  return $batch;
-}
-
-/**
- * Populates a job queue with the operations of a batch set.
- *
- * Depending on whether the batch is progressive or not, the
- * Drupal\Core\Queue\Batch or Drupal\Core\Queue\BatchMemory handler classes will
- * be used. The name and class of the queue are added by reference to the
- * batch set.
- *
- * @param $batch
- *   The batch array.
- * @param $set_id
- *   The id of the set to process.
- */
-function _batch_populate_queue(&$batch, $set_id) {
-  $batch_set = &$batch['sets'][$set_id];
-
-  if (isset($batch_set['operations'])) {
-    $batch_set += [
-      'queue' => [
-        'name' => 'drupal_batch:' . $batch['id'] . ':' . $set_id,
-        'class' => $batch['progressive'] ? 'Drupal\Core\Queue\Batch' : 'Drupal\Core\Queue\BatchMemory',
-      ],
-    ];
-
-    $queue = _batch_queue($batch_set);
-    $queue->createQueue();
-    foreach ($batch_set['operations'] as $operation) {
-      $queue->createItem($operation);
-    }
-
-    unset($batch_set['operations']);
-  }
-}
-
-/**
- * Returns a queue object for a batch set.
- *
- * @param $batch_set
- *   The batch set.
- *
- * @return
- *   The queue object.
- */
-function _batch_queue($batch_set) {
-  static $queues;
-
-  if (!isset($queues)) {
-    $queues = [];
-  }
-
-  if (isset($batch_set['queue'])) {
-    $name = $batch_set['queue']['name'];
-    $class = $batch_set['queue']['class'];
-
-    if (!isset($queues[$class][$name])) {
-      $queues[$class][$name] = new $class($name, \Drupal::database());
-    }
-    return $queues[$class][$name];
-  }
+  return BatchQueueController::getBatches();
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Batch/Batch.php b/core/lib/Drupal/Core/Batch/Batch.php
new file mode 100644
index 0000000..0c6b340
--- /dev/null
+++ b/core/lib/Drupal/Core/Batch/Batch.php
@@ -0,0 +1,443 @@
+<?php
+
+namespace Drupal\Core\Batch;
+
+/**
+ * Stores a domain object for a batch process.
+ *
+ * Example code to create a batch:
+ * @code
+ * $batch = Batch::create('Batch Title')
+ *   ->setTitle('Title can be changed.')
+ *   ->setFinishCallback('batch_example_finished_callback')
+ *   ->setInitMessage('The initialization message (optional)');
+ * for ($i = 0; $i < 1000; $i++) {
+ *   $batch->addOperation('batch_example_callback', array($i + 1));
+ * }
+ * $batch->enqueue();
+ * @endcode
+ */
+class Batch {
+
+  /**
+   * The set of operations to be processed.
+   *
+   * Each operation is a tuple of the function / method to use and an array
+   * containing any parameters to be passed.
+   *
+   * @var array
+   */
+  protected $operations = [];
+
+  /**
+   * The title for the batch.
+   *
+   * @var string
+   */
+  protected $title;
+
+  /**
+   * The initializing message for the batch.
+   *
+   * @var string
+   */
+  protected $initMessage;
+
+  /**
+   * The message to be shown while the batch is in progress.
+   *
+   * @var string
+   */
+  protected $progressMessage;
+
+  /**
+   * The message to be shown if a problem occurs.
+   *
+   * @var string
+   */
+  protected $errorMessage;
+
+  /**
+   * The name of a function / method to be called when the batch finishes.
+   *
+   * @var string
+   */
+  protected $finished;
+
+  /**
+   * The file containing the operation and finished callbacks.
+   *
+   * If the callbacks are in the .module file or are static methods of a class,
+   * then this does not need to be set.
+   *
+   * @var
+   */
+  protected $file;
+
+  /**
+   * An array of CSS file to be included when processing the batch.
+   *
+   * @var array
+   */
+  protected $css = [];
+
+  /**
+   * An array of options to be used with the redirect URL.
+   *
+   * @var array
+   */
+  protected $urlOptions = [];
+
+  /**
+   * Specifies if the batch is progressive.
+   *
+   * If true, multiple calls are used. Otherwise an attempt is made to process
+   * the batch in on page call.
+   *
+   * @var bool
+   */
+  protected $progressive = TRUE;
+
+  /**
+   * The details of the queue to use.
+   *
+   * A tuple containing the name of the queue and the class of the queue to use.
+   *
+   * @var array
+   */
+  protected $queue;
+
+  /**
+   * Constructs a new Batch instance.
+   */
+  public function __construct() {
+    $this->setTitle('Processing');
+    $this->setInitMessage('Initializing.');
+    $this->setProgressMessage('Completed @current of @total.');
+    $this->setErrorMessage('An error has occurred.');
+  }
+
+  /**
+   * Static constructor for a batch process.
+   *
+   * @return static
+   *   A new object.
+   */
+  public static function create() {
+    return new static();
+  }
+
+  /**
+   * Static constructor for a batch process with a title.
+   *
+   * @param string $title
+   *   The title.
+   *
+   * @return static
+   *   A new object.
+   */
+  public static function createFromTitle($title) {
+    return self::create()->setTitle($title);
+  }
+
+  /**
+   * Creates a \Drupal\Core\Batch\Batch object from an array.
+   *
+   * @param array $batch_definition
+   *   An array of values to use for the new object.
+   *
+   * @return static
+   *   A new initialized object.
+   */
+  public static function createFromArray(array $batch_definition) {
+    $new_batch = self::create();
+
+    if (isset($batch_definition['operations'])) {
+      foreach ($batch_definition['operations'] as list($callback, $arguments)) {
+        $new_batch->addOperation($callback, $arguments);
+      }
+    }
+
+    if (isset($batch_definition['title'])) {
+      $new_batch->setTitle($batch_definition['title']);
+    }
+
+    if (isset($batch_definition['init_message'])) {
+      $new_batch->setInitMessage($batch_definition['init_message']);
+    }
+
+    if (isset($batch_definition['progress_message'])) {
+      $new_batch->setProgressMessage($batch_definition['progress_message']);
+    }
+
+    if (isset($batch_definition['error_message'])) {
+      $new_batch->setErrorMessage($batch_definition['error_message']);
+    }
+
+    if (isset($batch_definition['finished'])) {
+      $new_batch->setFinishCallback($batch_definition['finished']);
+    }
+
+    if (isset($batch_definition['file'])) {
+      $new_batch->setFile($batch_definition['file']);
+    }
+
+    if (isset($batch_definition['css'])) {
+      if (is_array($batch_definition['css'])) {
+        $new_batch->setCss($batch_definition['css']);
+      }
+      else {
+        $new_batch->addCss($batch_definition['css']);
+      }
+    }
+
+    if (isset($batch_definition['url_options'])) {
+      $new_batch->setUrlOptions($batch_definition['url_options']);
+    }
+
+    if (isset($batch_definition['progressive'])) {
+      $new_batch->setProgressive($batch_definition['progressive']);
+    }
+
+    if (isset($batch_definition['queue'])) {
+      if (
+        isset($batch_definition['queue']['name']) &&
+        isset($batch_definition['queue']['class'])
+      ) {
+        $new_batch->setQueue(
+          $batch_definition['queue']['name'],
+          $batch_definition['queue']['class']
+        );
+      }
+    }
+
+    return $new_batch;
+  }
+
+  /**
+   * Sets the title.
+   *
+   * @param string $title
+   *   The title.
+   *
+   * @return $this
+   */
+  public function setTitle($title) {
+    $this->title = $title;
+    return $this;
+  }
+
+  /**
+   * Sets the finished callback.
+   *
+   * This callback will be executed if the batch process is done.
+   *
+   * @param string $callback
+   *   The callback.
+   *
+   * @return $this
+   */
+  public function setFinishCallback($callback) {
+    $this->finished = $callback;
+    return $this;
+  }
+
+  /**
+   * Sets the displayed message while processing is initialized.
+   *
+   * @param string $message
+   *   The text to display. Defaults to 'Initializing.'.
+   *
+   * @return $this
+   */
+  public function setInitMessage($message) {
+    $this->initMessage = $message;
+    return $this;
+  }
+
+  /**
+   * Sets the message to display when the batch is being processed.
+   *
+   * @param string $message
+   *   The text to display.  Available placeholders are '@current',
+   *   '@remaining', '@total', '@percentage', '@estimate' and '@elapsed'.
+   *   Defaults to 'Completed @current of @total.'.
+   *
+   * @return $this
+   */
+  public function setProgressMessage($message) {
+    $this->progressMessage = $message;
+    return $this;
+  }
+
+  /**
+   * Sets the message to display if an error occurs while processing.
+   *
+   * @param string $message
+   *   The text to display. Defaults to 'An error has occurred.'.
+   *
+   * @return $this
+   */
+  public function setErrorMessage($message) {
+    $this->errorMessage = $message;
+    return $this;
+  }
+
+  /**
+   * Sets the file that contains the callback functions.
+   *
+   * The path should be relative to base_path(), and thus should be built using
+   * drupal_get_path(). Defaults to {module_name}.module.
+   *
+   * @param string $filename
+   *   The path to the file.
+   *
+   * @return $this
+   */
+  public function setFile($filename) {
+    $this->file = $filename;
+    return $this;
+  }
+
+  /**
+   * Sets the location of CSS files.
+   *
+   * Adds the CSS files for use on the progress page. Any previously added CSS
+   * files are removed. Use \Drupal\Core\Batch\Batch::addCss() to add a file.
+   *
+   * @param string[] $filenames
+   *   The CSS files to be used.
+   *
+   * @return $this
+   */
+  public function setCss(array $filenames) {
+    $this->css = $filenames;
+    return $this;
+  }
+
+  /**
+   * Sets the options for redirect URLs.
+   *
+   * @param array $options
+   *   The options to use.
+   *
+   * @return $this
+   *
+   * @see \Drupal\Core\Url
+   */
+  public function setUrlOptions(array $options = []) {
+    $this->urlOptions = $options;
+    return $this;
+  }
+
+  /**
+   * Sets the batch to run progressively.
+   *
+   * @param bool $is_progressive
+   *   TRUE (default) indicates that the batch will run in more than one run.
+   *   FALSE indicates that the batch will finish in a single run.
+   *
+   * @return $this
+   */
+  public function setProgressive($is_progressive = TRUE) {
+    $this->progressive = $is_progressive;
+    return $this;
+  }
+
+  /**
+   * Sets or removes an override for the default queue.
+   *
+   * @param string $name
+   *   The unique identifier for the queue.
+   * @param string $class
+   *   The name of a class that implements \Drupal\Core\Queue\QueueInterface,
+   *   including the full namespace but not starting with a backslash. It must
+   *   have a constructor with two arguments: $name and a
+   *   \Drupal\Core\Database\Connection object. Typically, the class will either
+   *   be \Drupal\Core\Queue\Batch or \Drupal\Core\Queue\BatchMemory. Defaults
+   *   to Batch if progressive is TRUE, or to BatchMemory if progressive is
+   *   FALSE.
+   *
+   * @return $this
+   */
+  public function setQueue($name, $class) {
+    if (is_null($name) && is_null($class)) {
+      $this->queue = NULL;
+    }
+    else {
+      $this->queue = array(
+        'name' => $name,
+        'class' => $class,
+      );
+    }
+    return $this;
+  }
+
+  /**
+   * Adds a batch operation.
+   *
+   * @param string $callback
+   *   The name of the callback function.
+   * @param array $arguments
+   *   An array of arguments to pass to the callback function.
+   *
+   * @return $this
+   */
+  public function addOperation($callback, $arguments = []) {
+    $this->operations[] = [$callback, $arguments];
+    return $this;
+  }
+
+  /**
+   * Adds a CSS file to be used on the process page.
+   *
+   * @param string $filename
+   *   The CSS file to use.
+   *
+   * @return $this
+   */
+  public function addCss($filename) {
+    $this->css[] = $filename;
+    return $this;
+  }
+
+  /**
+   * Places the batch in the queue to be processed.
+   *
+   * This is an alias for BatchQueueController::enqueue() to allow for
+   * pipe-lining of Batch objects.
+   *
+   * @return $this
+   *
+   * @see \Drupal\Core\Batch\BatchQueueController::enqueue()
+   */
+  public function enqueue() {
+    BatchQueueController::enqueue($this);
+    return $this;
+  }
+
+  /**
+   * Converts a \Drupal\Core\Batch\Batch object into an array.
+   *
+   * @return array
+   *   The array representation of the object.
+   */
+  public function toArray() {
+    $array = [
+      'operations' => $this->operations ?: [],
+      'title' => $this->title ?: '',
+      'init_message' => $this->initMessage ?: '',
+      'progress_message' => $this->progressMessage ?: '',
+      'error_message' => $this->errorMessage ?: '',
+      'finished' => $this->finished,
+      'file' => $this->file,
+      'css' => $this->css ?: [],
+      'url_options' => $this->urlOptions ?: [],
+      'progressive' => $this->progressive,
+      'queue' => $this->queue ?: NULL,
+    ];
+
+    return $array;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Batch/BatchQueueController.php b/core/lib/Drupal/Core/Batch/BatchQueueController.php
new file mode 100644
index 0000000..046da09
--- /dev/null
+++ b/core/lib/Drupal/Core/Batch/BatchQueueController.php
@@ -0,0 +1,597 @@
+<?php
+
+namespace Drupal\Core\Batch;
+
+use Drupal\Component\Utility\Timer;
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Queue\Batch as BatchQueue;
+use Drupal\Core\Queue\BatchMemory;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Url;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+/**
+ * Handles operations on the batch processes to be handled.
+ */
+class BatchQueueController {
+
+  /**
+   * The batch sets to be processed.
+   *
+   * Used as a memory cache.
+   *
+   * @var array
+   */
+  protected static $batches = [];
+
+  /**
+   * The queue objects to be processed.
+   *
+   * Used as a memory cache.
+   *
+   * @var array
+   */
+  protected static $queues = [];
+
+  /**
+   * Places the batch in the queue to be processed.
+   *
+   * Batch objects are also queued by invoking <code>enqueue()</code> directly
+   * on the object.
+   *
+   * @param \Drupal\Core\Batch\Batch $batch
+   *   The batch to place in the queue.
+   *
+   * @see Drupal\Core\Batch\Batch::enqueue()
+   */
+  public static function enqueue(Batch $batch) {
+    // Initialize the batch if needed.
+    if (empty(self::$batches)) {
+      self::$batches = array(
+        'sets' => array(),
+        'has_form_submits' => FALSE,
+      );
+    }
+
+    $init = [
+      'sandbox' => [],
+      'results' => [],
+      'success' => FALSE,
+      'start' => 0,
+      'elapsed' => 0,
+    ];
+    $batch_set = $init + $batch->toArray();
+
+    // Make strings translatable.
+    if (is_string($batch_set['title'])) {
+      $batch_set['title'] = new TranslatableMarkup($batch_set['title']);
+    }
+    if (is_string($batch_set['init_message'])) {
+      $batch_set['init_message'] = new TranslatableMarkup($batch_set['init_message'] . '<br />&nbsp;');
+    }
+    elseif ($batch_set['init_message'] instanceof TranslatableMarkup) {
+      $batch_set['init_message'] = new TranslatableMarkup(
+        $batch_set['init_message']->getUntranslatedString() . '<br />&nbsp;',
+        $batch_set['init_message']->getArguments()
+      );
+    }
+    if (is_string($batch_set['progress_message'])) {
+      $batch_set['progress_message'] = new TranslatableMarkup($batch_set['progress_message']);
+    }
+    if (is_string($batch_set['error_message'])) {
+      $batch_set['error_message'] = new TranslatableMarkup($batch_set['error_message']);
+    }
+
+    // The non-concurrent workflow of batch execution allows us to save
+    // numberOfItems() queries by handling our own counter.
+    $batch_set['total'] = count($batch_set['operations']);
+    $batch_set['count'] = $batch_set['total'];
+    // Add the set to the batch.
+    if (empty(self::$batches['id'])) {
+      // The batch is not running yet. Simply add the new set.
+      self::$batches['sets'][] = $batch_set;
+    }
+    else {
+      // The set is being added while the batch is running. Insert the new set
+      // right after the current one to ensure execution order, and store its
+      // operations in a queue.
+      $index = self::$batches['current_set'] + 1;
+      $slice1 = array_slice(self::$batches['sets'], 0, $index);
+      $slice2 = array_slice(self::$batches['sets'], $index);
+      self::$batches['sets'] = array_merge($slice1, array($batch_set), $slice2);
+      self::populateQueue($index);
+    }
+  }
+
+  /**
+   * Processes the batch.
+   *
+   * This function is generally not needed in form submit handlers;
+   * Form API takes care of batches that were set during form submission.
+   *
+   * @param \Drupal\Core\Url|string $redirect
+   *   (optional) Either path or Url object to redirect to when the batch has
+   *   finished processing. Note that to simply force a batch to (conditionally)
+   *   redirect to a custom location after it is finished processing but to
+   *   otherwise allow the standard form API batch handling to occur, it is not
+   *   necessary to call batch_process() and use this parameter. Instead, make
+   *   the batch 'finished' callback return an instance of
+   *   \Symfony\Component\HttpFoundation\RedirectResponse, which will be used
+   *   automatically by the standard batch processing pipeline (and which takes
+   *   precedence over this parameter).
+   * @param \Drupal\Core\Url $url
+   *   (optional) URL of the batch processing page.
+   *   Should only be used for separate scripts like update.php.
+   * @param string $redirect_callback
+   *   (optional) Specify a function to be called to redirect to the progressive
+   *   processing page.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse|null
+   *   A redirect response if the batch is progressive. No return value
+   *   otherwise.
+   */
+  public static function process($redirect = NULL, Url $url = NULL, $redirect_callback = NULL) {
+    if (isset(self::$batches)) {
+      // Add process information.
+      $process_info = [
+        'current_set' => 0,
+        'progressive' => TRUE,
+        'url' => isset($url) ? $url : Url::fromRoute('system.batch_page.html'),
+        'source_url' => Url::fromRouteMatch(\Drupal::routeMatch()),
+        'batch_redirect' => $redirect,
+        'theme' => \Drupal::theme()->getActiveTheme()->getName(),
+        'redirect_callback' => $redirect_callback,
+      ];
+      self::$batches += $process_info;
+
+      // The batch is now completely built. Allow other modules to make changes
+      // to the batch so that it is easier to reuse batch processes in other
+      // environments.
+      \Drupal::moduleHandler()->alter('batch', self::$batches);
+
+      // Assign an arbitrary id: don't rely on a serial column in the 'batch'
+      // table, since non-progressive batches skip database storage completely.
+      self::$batches['id'] = Database::getConnection()->nextId();
+
+      // Move operations to a job queue. Non-progressive batches will use a
+      // memory-based queue.
+      foreach (self::$batches['sets'] as $key => $batch_set) {
+        self::populateQueue($key);
+      }
+
+      // Initiate processing.
+      if (self::$batches['progressive']) {
+        // Now that we have a batch id, we can generate the redirection link in
+        // the generic error message.
+        /** @var \Drupal\Core\Url $batch_url */
+        $batch_url = self::$batches['url'];
+        /** @var \Drupal\Core\Url $error_url */
+        $error_url = clone $batch_url;
+        $query_options = $error_url->getOption('query');
+        $query_options['id'] = self::$batches['id'];
+        $query_options['op'] = 'finished';
+        $error_url->setOption('query', $query_options);
+
+        self::$batches['error_message'] = t('Please continue to <a href=":error_url">the error page</a>', array(':error_url' => $error_url->toString(TRUE)->getGeneratedUrl()));
+
+        // Clear the way for the redirection to the batch processing page, by
+        // saving and un-setting the 'destination', if there is any.
+        $request = \Drupal::request();
+        if ($request->query->has('destination')) {
+          self::$batches['destination'] = $request->query->get('destination');
+          $request->query->remove('destination');
+        }
+
+        // Store the batch.
+        \Drupal::service('batch.storage')->create(self::$batches);
+
+        // Set the batch number in the session to guarantee that it will stay
+        // alive.
+        $_SESSION['batches'][self::$batches['id']] = TRUE;
+
+        // Redirect for processing.
+        $query_options = $error_url->getOption('query');
+        $query_options['op'] = 'start';
+        $query_options['id'] = self::$batches['id'];
+        $batch_url->setOption('query', $query_options);
+        if (($function = self::$batches['redirect_callback']) && function_exists($function)) {
+          $function($batch_url->toString(), ['query' => $query_options]);
+        }
+        else {
+          return new RedirectResponse($batch_url->setAbsolute()->toString(TRUE)->getGeneratedUrl());
+        }
+      }
+      else {
+        // Non-progressive execution: bypass the whole progressbar workflow
+        // and execute the batch in one pass.
+        self::processQueue();
+      }
+    }
+
+    return NULL;
+  }
+
+  /**
+   * Retrieves the current batch.
+   */
+  public static function &getBatches() {
+    return self::$batches;
+  }
+
+  /**
+   * Returns a queue object for a batch set.
+   *
+   * @param array $batch
+   *   The batch set.
+   *
+   * @return \Drupal\Core\Queue\QueueInterface|null
+   *   The queue object or null if the queue cannot be created.
+   */
+  public static function getQueueForBatch($batch) {
+    if (isset($batch['queue'])) {
+      return self::getQueue($batch['queue']['name'], $batch['queue']['class']);
+    }
+    return NULL;
+  }
+
+  /**
+   * Returns the batch set being currently processed.
+   *
+   * @return array
+   *   The current batch set.
+   */
+  public static function &getCurrentSet() {
+    return self::$batches['sets'][self::$batches['current_set']];
+  }
+
+  /**
+   * Retrieves the next set in a batch.
+   *
+   * If there is a subsequent set in this batch, assign it as the new set to
+   * process and execute its form submit handler (if defined), which may add
+   * further sets to this batch.
+   *
+   * @return bool
+   *   TRUE if a subsequent set was found in the batch; FALSE will be returned
+   *   if no subsequent set was found.
+   */
+  public static function nextSet() {
+    if (isset(self::$batches['sets'][self::$batches['current_set'] + 1])) {
+      self::$batches['current_set']++;
+      $current_set = &self::getCurrentSet();
+      if (isset($current_set['form_submit']) && ($callback = $current_set['form_submit']) && is_callable($callback)) {
+        // We use our stored copies of $form and $form_state to account for
+        // possible alterations by previous form submit handlers.
+        $complete_form = &self::$batches['form_state']->getCompleteForm();
+        call_user_func_array($callback, array(&$complete_form, &self::$batches['form_state']));
+      }
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Processes sets in a batch.
+   *
+   * If the batch was marked for progressive execution (default), this executes
+   * as many operations in batch sets until an execution time of 1 second has
+   * been exceeded. It will continue with the next operation of the same batch
+   * set in the next request.
+   *
+   * @return array
+   *   An array containing a completion value (in percent) and a status message.
+   */
+  public static function processQueue() {
+    $current_set = &self::getCurrentSet();
+    // Indicate that this batch set needs to be initialized.
+    $set_changed = TRUE;
+
+    // If this batch was marked for progressive execution (e.g. forms submitted
+    // by \Drupal::formBuilder()->submitForm(), initialize a timer to determine
+    // whether we need to proceed with the same batch phase when a processing
+    // time of 1 second has been exceeded.
+    if (self::$batches['progressive']) {
+      Timer::start('batch_processing');
+    }
+
+    if (empty($current_set['start'])) {
+      $current_set['start'] = microtime(TRUE);
+    }
+
+    $queue = self::getQueueForBatch($current_set);
+
+    while (!$current_set['success']) {
+      // If this is the first time we iterate this batch set in the current
+      // request, we check if it requires an additional file for functions
+      // definitions.
+      if ($set_changed && isset($current_set['file']) && is_file($current_set['file'])) {
+        include_once \Drupal::root() . '/' . $current_set['file'];
+      }
+
+      $task_message = $label = '';
+      // Assume a single pass operation and set the completion level to 1 by
+      // default.
+      $finished = 1;
+
+      if ($item = $queue->claimItem()) {
+        list($callback, $args) = $item->data;
+
+        // Build the 'context' array and execute the function call.
+        $batch_context = array(
+          'sandbox'  => &$current_set['sandbox'],
+          'results'  => &$current_set['results'],
+          'finished' => &$finished,
+          'message'  => &$task_message,
+        );
+        call_user_func_array($callback, array_merge($args, array(&$batch_context)));
+
+        if ($finished >= 1) {
+          // Make sure this step is not counted twice when computing $current.
+          $finished = 0;
+          // Remove the processed operation and clear the sandbox.
+          $queue->deleteItem($item);
+          $current_set['count']--;
+          $current_set['sandbox'] = array();
+        }
+      }
+
+      // When all operations in the current batch set are completed, browse
+      // through the remaining sets, marking them 'successfully processed'
+      // along the way, until we find a set that contains operations.
+      // _batch_next_set() executes form submit handlers stored in 'control'
+      // sets (see \Drupal::service('form_submitter')), which can in turn add
+      // new sets to the batch.
+      $set_changed = FALSE;
+      $old_set = $current_set;
+      while (empty($current_set['count']) && ($current_set['success'] = TRUE) && self::nextSet()) {
+        $current_set = &self::getCurrentSet();
+        $current_set['start'] = microtime(TRUE);
+        $set_changed = TRUE;
+      }
+
+      // At this point, either $current_set contains operations that need to be
+      // processed or all sets have been completed.
+      $queue = self::getQueueForBatch($current_set);
+
+      // If we are in progressive mode, break processing after 1 second.
+      if (self::$batches['progressive'] && Timer::read('batch_processing') > 1000) {
+        // Record elapsed wall clock time.
+        $current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
+        break;
+      }
+    }
+
+    if (self::$batches['progressive']) {
+      // Gather progress information.
+      //
+      // Reporting 100% progress will cause the whole batch to be considered
+      // processed. If processing was paused right after moving to a new set,
+      // we have to use the info from the new (unprocessed) set.
+      if ($set_changed && isset($current_set['queue'])) {
+        // Processing will continue with a fresh batch set.
+        $remaining        = $current_set['count'];
+        $total            = $current_set['total'];
+        $progress_message = $current_set['init_message'];
+        $task_message     = '';
+      }
+      else {
+        // Processing will continue with the current batch set.
+        $remaining        = $old_set['count'];
+        $total            = $old_set['total'];
+        $progress_message = $old_set['progress_message'];
+      }
+
+      // Total progress is the number of operations that have fully run plus the
+      // completion level of the current operation.
+      $current    = $total - $remaining + $finished;
+      $percentage = Percentage::format($total, $current);
+      $elapsed    = isset($current_set['elapsed']) ? $current_set['elapsed'] : 0;
+      $values     = array(
+        '@remaining'  => $remaining,
+        '@total'      => $total,
+        '@current'    => floor($current),
+        '@percentage' => $percentage,
+        '@elapsed'    => \Drupal::service('date.formatter')->formatInterval($elapsed / 1000),
+        // If possible, estimate remaining processing time.
+        '@estimate'   => ($current > 0) ? \Drupal::service('date.formatter')->formatInterval(($elapsed * ($total - $current) / $current) / 1000) : '-',
+      );
+      $message = strtr($progress_message, $values);
+      if (!empty($task_message)) {
+        $label = $task_message;
+      }
+
+      return array($percentage, $message, $label);
+    }
+    else {
+      // If we are not in progressive mode, the entire batch has been processed.
+      return self::finishedProcessing();
+    }
+  }
+
+  /**
+   * Ends the batch processing.
+   *
+   * Call the 'finished' callback of each batch set to allow custom handling of
+   * the results and resolve page redirection.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse|false
+   *   A redirect response to the completed page or NULL to stay at the current
+   *   URL.
+   */
+  public static function finishedProcessing() {
+    self::$batches = &batch_get();
+    $batch_finished_redirect = NULL;
+
+    // Execute the 'finished' callbacks for each batch set, if defined.
+    foreach (self::$batches['sets'] as $batch_set) {
+      if (isset($batch_set['finished'])) {
+        // Check if the set requires an additional file for function
+        // definitions.
+        if (isset($batch_set['file']) && is_file($batch_set['file'])) {
+          include_once \Drupal::root() . '/' . $batch_set['file'];
+        }
+        if (is_callable($batch_set['finished'])) {
+          $queue = self::getQueueForBatch($batch_set);
+          $operations = $queue->getAllItems();
+          $batch_set_result = call_user_func_array(
+            $batch_set['finished'],
+            array(
+              $batch_set['success'],
+              $batch_set['results'],
+              $operations,
+              \Drupal::service('date.formatter')
+                ->formatInterval($batch_set['elapsed'] / 1000),
+            )
+          );
+          // If a batch 'finished' callback requested a redirect after the batch
+          // is complete, save that for later use. If more than one batch set
+          // returned a redirect, the last one is used.
+          if ($batch_set_result instanceof RedirectResponse) {
+            $batch_finished_redirect = $batch_set_result;
+          }
+        }
+      }
+    }
+
+    // Clean up the batch table and unset the static $batch variable.
+    if (self::$batches['progressive']) {
+      \Drupal::service('batch.storage')->delete(self::$batches['id']);
+      foreach (self::$batches['sets'] as $batch_set) {
+        if ($queue = self::getQueueForBatch($batch_set)) {
+          $queue->deleteQueue();
+        }
+      }
+      // Clean-up the session. Not needed for CLI updates.
+      if (isset($_SESSION)) {
+        unset($_SESSION['batches'][self::$batches['id']]);
+        if (empty($_SESSION['batches'])) {
+          unset($_SESSION['batches']);
+        }
+      }
+    }
+    $_batch = self::$batches;
+    self::$batches = NULL;
+
+    // Redirect if needed.
+    if ($_batch['progressive']) {
+      // Revert the 'destination' that was saved in batch_process().
+      if (isset($_batch['destination'])) {
+        \Drupal::request()->query->set('destination', $_batch['destination']);
+      }
+
+      // Determine the target path to redirect to. If a batch 'finished'
+      // callback returned a redirect response object, use that. Otherwise, fall
+      // back on the form redirection.
+      if (isset($batch_finished_redirect)) {
+        return $batch_finished_redirect;
+      }
+      elseif (!isset($_batch['form_state'])) {
+        $_batch['form_state'] = new FormState();
+      }
+      if ($_batch['form_state']->getRedirect() === NULL) {
+        $redirect = $_batch['batch_redirect'] ?: $_batch['source_url'];
+        // Any path with a scheme does not correspond to a route.
+        if (!$redirect instanceof Url) {
+          $options = UrlHelper::parse($redirect);
+          if (parse_url($options['path'], PHP_URL_SCHEME)) {
+            $redirect = Url::fromUri($options['path'], $options);
+          }
+          else {
+            $redirect = \Drupal::pathValidator()->getUrlIfValid($options['path']);
+            if (!$redirect) {
+              // Stay on the same page if the redirect was invalid.
+              $redirect = Url::fromRoute('<current>');
+            }
+            $redirect->setOptions($options);
+          }
+        }
+        $_batch['form_state']->setRedirectUrl($redirect);
+      }
+
+      // Use \Drupal\Core\Form\FormSubmitterInterface::redirectForm() to handle
+      // the redirection logic.
+      $redirect = \Drupal::service('form_submitter')->redirectForm($_batch['form_state']);
+      if (is_object($redirect)) {
+        return $redirect;
+      }
+
+      // If no redirection happened, redirect to the originating page. In case
+      // the form needs to be rebuilt, save the final $form_state for
+      // \Drupal\Core\Form\FormBuilderInterface::buildForm().
+      if ($_batch['form_state']->isRebuilding()) {
+        $_SESSION['batch_form_state'] = $_batch['form_state'];
+      }
+      $callback = $_batch['redirect_callback'];
+      /** @var \Drupal\Core\Url $source_url */
+      $source_url = $_batch['source_url'];
+      if (is_callable($callback)) {
+        $callback($_batch['source_url'], array('query' => array('op' => 'finish', 'id' => $_batch['id'])));
+      }
+      elseif ($callback === NULL) {
+        // Default to RedirectResponse objects when nothing specified.
+        $url = $source_url
+          ->setAbsolute()
+          ->setOption('query', ['op' => 'finish', 'id' => $_batch['id']]);
+        return new RedirectResponse($url->toString());
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Returns a queue object with the given name and class.
+   *
+   * @param string $name
+   *   The name of the queue.
+   * @param string $class
+   *   The class of the queue.
+   *   Usually \Drupal\Core\Queue\Batch or \Drupal\Core\Queue\BatchMemory.
+   *
+   * @return \Drupal\Core\Queue\QueueInterface
+   *   The queue object.
+   */
+  protected static function getQueue($name, $class) {
+    if (!isset(self::$queues[$class][$name])) {
+      self::$queues[$class][$name] = new $class($name, \Drupal::database());
+    }
+    return self::$queues[$class][$name];
+  }
+
+  /**
+   * Populates a job queue with the operations of a batch set.
+   *
+   * Depending on whether the batch is progressive or not, the
+   * Drupal\Core\Queue\Batch or Drupal\Core\Queue\BatchMemory handler classes
+   * will be used. The name and class of the queue are added by reference to the
+   * batch set.
+   *
+   * @param int $set_id
+   *   The id of the set to process.
+   */
+  protected static function populateQueue($set_id = 0) {
+    $batch = &self::$batches;
+    $batch_set = &$batch['sets'][$set_id];
+
+    if (isset($batch_set['operations'])) {
+      if (empty($batch_set['queue'])) {
+        $batch_set['queue'] = [
+          'name' => 'drupal_batch:' . $batch['id'] . ':' . $set_id,
+          'class' => (
+          $batch['progressive'] ? BatchQueue::class : BatchMemory::class
+          ),
+        ];
+      }
+
+      $queue = self::getQueueForBatch($batch_set);
+      $queue->createQueue();
+      foreach ($batch_set['operations'] as $operation) {
+        $queue->createItem($operation);
+      }
+
+      unset($batch_set['operations']);
+    }
+  }
+
+}
