2

I'm using a batch job in Drupal 8 to scrape pages from another site to import.

I'm using httpClient, and I have it wrapped in a try/catch.

try {
  $response = \Drupal::httpClient()->get($parts['scheme'] . '://' . $parts['host'] . $parts['path'], [
    'query' => $parts['query'],
    'headers' => array('Accept'=>'text/xml'),
    'verify' => FALSE,
    'connect_timeout' => 6,
    'CURLOPT_SSL_VERIFYPEER' => false,
    'CURLOPT_CERTINFO' => false,
  ]);
  return (string) $response->getBody();
}
catch (Exception $e) {
  watchdog_exception('scrape', $e);
  return FALSE;
}

However, from time to time the remote site times out, and the guzzle exception is not caught, which causes the batch job to end.

An AJAX HTTP error occurred.
HTTP Result Code: 500
Debugging information follows.
Path: /batch?id=44&op=do_nojs&op=do
StatusText: Internal Server Error
ResponseText: The website encountered an unexpected error. Please try again later.GuzzleHttp\Exception\ConnectException: cURL error 28: Operation timed out after 30001 milliseconds with 0 bytes received (see http://curl.haxx.se/libcurl/c/libcurl-errors.html) in GuzzleHttp\Handler\CurlFactory::createRejection() (line 186 of vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php). GuzzleHttp\Handler\CurlFactory::finishError(Object, Object, Object) (Line: 103)

This job imports a few thousand pages, so when the error happens in the middle of the batch, it's a major pain.

I am getting ready to write a queue for the batch processor, where the batch pops a set of items off the queue and then marks them as done, so that I can continue the job after an exception.

But then I wondered, is there already a way to tell Drupal to continue a batch from where it left off or was interrupted? I did some googling and didn't find anything indicating that this feature already existed. I don't want to re-invent the wheel if I don't have to.

Can I re-start an incomplete, interrupted batch job?

2
  • were you able to figure this out, I am interested in the answer as well.
    – awm
    Feb 2, 2018 at 15:07
  • @awm I created a module that does batch queuing, I will try to release it shortly.
    – user1359
    Feb 2, 2018 at 15:20

1 Answer 1

0

So I think there is no way to tell batch to restart when it's gotten an error response. What you could do is to not return a possible error but rather re-try the request using your own wrapper. So could do

try {

  $TestRetry = new TestRetry();
  return $TestRetry->test();
}
// And I think you won't need this but handle the exception in your wrapper class.
catch (Exception $e) {
  watchdog_exception('scrape', $e);
  return FALSE;
}

and in your wrapper, you could attempt to retry the request to avoid an error. So your wrapper may look like this:

<?php

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Handler\CurlHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;

class TestRetry {

  public function test() {
    $handlerStack = HandlerStack::create(new CurlHandler());
    $handlerStack->push(Middleware::retry($this->retryDecider(), $this->retryDelay()));
    $client = new Client(array('handler' => $handlerStack));

    $response = $client->request(
      'GET',
// @todo replace to a real url!!!
      'https://500-error-code-url'
    )->getBody()->getContents();

    return \GuzzleHttp\json_decode($response, TRUE);
  }


  public function retryDecider() {
    return function (
      $retries,
      Request $request,
      Response $response = NULL,
      RequestException $exception = NULL
    ) {
// Limit the number of retries to 5
      if ($retries >= 5) {
        return FALSE;
      }

// Retry connection exceptions
      if ($exception instanceof ConnectException) {
        return TRUE;
      }

      if ($response) {
// Retry on server errors
        if ($response->getStatusCode() >= 500) {
          return TRUE;
        }
      }

      return FALSE;
    };
  }

  /**
   * delay 1s 2s 3s 4s 5s
   *
   * @return Closure
   */
  public function retryDelay() {
    return function ($numberOfRetries) {
      return 1000 * $numberOfRetries;
    };
  }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.