Batch operation keeps looping after completion The batch operation I created works fine, until it's done.  Once it's done it just loops back and does it again.  It will loop through until the VM runs out of resources.  

I am fairly certain this has to do with my completeImport method.  If I throw a syntax error in there it will finish the import successfully but then error out once it gets to that method.

The controller for the batch op: (I ommited methods that had nothing to do with the batch itself.  They were for CRUD or building URLs.)

class MyModuleController {

  public function import() {
    $endpoints = array(
      'courses' =&gt; $this-&gt;getEndPoint('courses', NULL),
    );

    foreach ($endpoints as $data_type =&gt; $endpoint) {
      $response = $this-&gt;getResponse($endpoint);
      $operations = MyModuleController::processResponse($response, $data_type);

      $batch = array(
        'title' =&gt; 'Importing ' . $data_type,
        'operations' =&gt; $operations,
        'finished' =&gt; 'mymodule_complete_import',
        'init_message' =&gt; t('Importing...'),
        'progress_message' =&gt; t('Operation @current out of @total.'),
        'error_message' =&gt; t('Batch failed.'),
        'file' =&gt; drupal_get_path('module', 'mymodule') . '/mymodule.import.inc',
      );

      batch_set($batch);

      return batch_process('academy/courses/import');
    }

    return array(
      '#type' =&gt; 'markup',
      'value' =&gt; t('Imported?'),
    );
  }

  public static function processResponse($response_data, $data_type) {
    $operations = array();

    foreach ($response_data as $data) {
      switch ($data_type) {
        case 'courses':
          // Used to look up if a course was already imported.
          $id = (string) $data-&gt;Id;

          $data_array = array(
            'Name'                      =&gt; (string) $data-&gt;Name,
            'Active'                    =&gt; ((string) $data-&gt;Active == 'true' ? 1 : 0),
            'CourseCodeForBulkImport'   =&gt; (string) $data-&gt;CourseCodeForBulkImport,
            'EcommerceLongDescription'  =&gt; (string) $data-&gt;EcommerceLongDescription,
            'EcommerceShortDescription' =&gt; (string) $data-&gt;EcommerceShortDescription,
            'ForSale'                   =&gt; ((string) $data-&gt;ForSale == 'true' ? 1 : 0),
            'Code'                      =&gt; (string) $data-&gt;Code,
            'Id'                        =&gt; $id,
            'OriginalId'                =&gt; (string) $data-&gt;OriginalId,
            'Price'                     =&gt; (string) $data-&gt;Price,
            'Description'               =&gt; (string) $data-&gt;Description,
          );

          break;
      }

      $operations[] = array(
        'mymodule_response_importer',
        array($id, $data_array, $data_type),
      );
    }

    return $operations;
  }

  public static function completeImport() {
    return new RedirectResponse(Url::fromRoute('system.admin_content'));
  }

}


mymodule.import.inc:

function mymodule_response_importer($id, $data, $data_type, &amp;$context) {
  $entity = MyModuleController::findExisting($data_type, $id);

  switch ($data_type) {
    case 'courses':
      if ($entity === FALSE) {
        MyModuleController::createNode($data);
      }
      else {
        MyModuleController::updateNode($data, NULL, $entity);
      }

      break;
  }
}

function mymodule_complete_import() {
  MyModuleController::completeImport();
}
I was able to fix it by using the magic send() method for RedirectResponse class and building a full URL (mostly) manually.

  public static function completeImport() {

    $url = $GLOBALS['base_secure_url'];
    $url .= $GLOBALS['base_path'];
    $url .= Url::fromRoute('system.admin_content')-&gt;getInternalPath();
    $url .= '?status=All&amp;type=academy_webinar&amp;title=&amp;langcode=All';

    $redirect = new RedirectResponse($url);

    $redirect-&gt;send();
  }