Programmatically invoke cron in background

Created on 19 November 2020, almost 4 years ago
Updated 16 March 2023, over 1 year ago

Problem/Motivation

I have an eCommerce site that, on order being placed and paid for - we need to push the order contents into an ERP system via API's. The workflow is as follows:

  1. Order is placed
  2. Payment gateway returns payment success
  3. Event subscriber listening for "commerce_order.place.post_transition" is fired
  4. The order ID is pushed into a Drupal Queue (very cool)
  5. Cron is run
  6. The Queue is processed - pushing the full order data into a third party system

I wanted to decouple the API part from the order being placed - as in the past - using the same thread that comes back from a payment processor can cause havoc with the gateway deeming the transaction as invalid. The Drupal Queue system is perfect for this.

If I have cron running via crontab every 60 seconds - the longest we would be waiting for the order to be pushed (and emails sent) - could be up to 60 seconds. Which I see as room for improvement.

It would be great if I could kick off a "cron run" from within my order confirmation event subscriber:

$cron = Drupal::service('cron');
$cron->run();

But running the above then hangs the entire process.

I have instead been delving into the world of "fire and forget PHP requests" - and have come up with the following class:

use Drupal;
use Drupal\Core\Url;

class CronFireForget {

  private function backgroundPost($url) {
    $parts = parse_url($url);
    $fp = fsockopen($parts['host'], isset($parts['port']) ? $parts['port'] : 80, $errno, $errstr, 2);

    if (!$fp) {
      return FALSE;
    }
    else {
      $out = "GET " . $parts['path'] . " HTTP/1.1\r\n";
      $out .= "Host: " . $parts['host'] . "\r\n";
      $out .= "User-Agent: Background Cron v1.1.0\r\n";
      $out .= "Content-Type: text/html\r\n";
      $out .= "Connection: Close\r\n\r\n";
      fwrite($fp, $out);
      fclose($fp);
      return TRUE;
    }
  }

  public function fireAndLogCron() {

    $timer = microtime(TRUE);
    $cron_url = Url::fromRoute('system.cron', [
      'key' => Drupal::state()
        ->get('system.cron_key'),
    ], ['absolute' => TRUE])->toString();
    dump($cron_url);
    $boolResponse = $this->backgroundPost($cron_url);
    //$boolResponse = TRUE;
    //$cron = Drupal::service('cron');
    //$cron->run();
    $timerEnd = microtime(TRUE);
    $elapsed = intval(($timerEnd - $timer) * 1000);
    Drupal::logger('background_cron')
      ->info("Backgroud post; " . $boolResponse . ", took: " . $elapsed . " millisecs");
  }

}

This is the only thing I found that does what is required (tried Symfony HttpClient) - but it feels a little off to be doing it this way. Can anybody chime in with other ideas / approaches - or risks around the solution I'm currently testing above (apache memory leaks etc. / issue with cron itself running)

💬 Support request
Status

Fixed

Component

Code

Created by

🇬🇧United Kingdom newaytech

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • 🇬🇧United Kingdom newaytech

    Hope I'm not breaking the rules by raising this issue again - but the code that was running perfectly (DestructableInterface to run cron) - alas is now throwing an error:

    RuntimeException: Failed to start the session because headers have already been sent by "C:\xampp\apache\htdocs\drupal-8\neway-drupal-project\vendor\symfony\http-foundation\Response.php" at line 1239. in Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->start() (line 152 of C:\xampp\apache\htdocs\drupal-8\neway-drupal-project\vendor\symfony\http-foundation\Session\Storage\NativeSessionStorage.php).

    I've even disabled every other cron task to try and isolate - but to no avail.

    I can get this working again by rolling back my code base back to prior to Feb (Upgrading drupal/core (9.5.2 => 9.5.3))

    Is there a better way to invoke an immediate queue processing run - whilst still having the securiyy of any failures getting picked up by cron (and also having the item only processed once?)

Production build 0.71.5 2024