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:
- Order is placed
- Payment gateway returns payment success
- Event subscriber listening for "commerce_order.place.post_transition" is fired
- The order ID is pushed into a Drupal Queue (very cool)
- Cron is run
- 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)