2

My goal is to create a function in Ajax to send post data to a PHP script on a different server. The intended result is to have the PHP script disconnect from the client and run the entire PHP scrip in the background on the server, to allow the user to navigate freely, and not have them wait for the PHP script to finish. I have successfully posted the data to the PHP script, but I cannot get the PHP script to continue running after disconnecting from the client. I do not want or need the PHP script to output anything to any other scripts. I just need it to run in the background.

I have tried using ignore_user_abort(true), header("Connection: close\r\n") and other connection handling in PHP, but have been unsuccessful. This is my latest attempt at achieving the desired functionality.

Sending script

<script>
    function sendingRequest()
    {
        var form = document.getElementById("submit_change");
        var var1 = document.getElementById('var1').value;
        var var2 = document.getElementById('var2').value;
        var var3 = document.getElementById('var3').value;
        var dataString = 'var1='+ var1 + '&var2=' + var2 + '&var3=' + var3;
        $.ajax({
            type:"post",
            url:"recievingScript.php",
            data:dataString,
            cache:false
        });
        form.submit();
        return false;
    }
</script>

PHP script

<?php
ignore_user_abort(true);
//includes, uses, requires, establish db connection
$var1 = $_POST['var1'];
$var2 = $_POST['var2'];
$var3 = $_POST['var3'];

header("Access-Control-Allow-Origin: *");
header("Connection: close");

//Code to be run

//end script
?>

With a success: in the Ajax post the PHP script successfully runs and sends a success message to the script that called the PHP script. But the script takes about 10 seconds to run, so it is not a good idea to make the user wait for the script to finish. Any advice is appreciated, thanks!

Dagroa
  • 43
  • 5
  • 1
    Sadly AJAX/PHP is not stateless, so the client waits for the response. It sounds a lot like your looking for a Message Broker system. I suggest looking into a framework like [RabbitMQ](https://www.rabbitmq.com/tutorials/tutorial-one-php.html) or [ZeroMQ](http://zeromq.org/bindings:php). For a non-php approach, check into [node.js + socket.io](https://socket.io/get-started/chat/) or [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) solutions. The concept is a client can send a message request and simply disconnect, while a separate system can listen for a response. – Will B. Jan 15 '19 at 05:14
  • Really? dang, no wonder I spent like 5 hours trying to make it work yesterday. The issue is that I am basically handcuffed to using PHP for this, Ill have to look into RabbitMQ or ZeroMQ. Thanks! – Dagroa Jan 15 '19 at 12:33
  • What version of PHP are you using, and what environment is PHP being hosted under (Windows, Mac, Linux, Apache, IIS, NGINX)? Are you using a Database Management System like MySQL, PostgreSQL or MongoDB? Also what exactly is happening that lasts 10 seconds? – Will B. Jan 15 '19 at 16:46
  • The issue is that AJAX needs to know that a connection was established and that the full request was received by the recipient. Otherwise you run the risk of losing data and corrupted information being received. You could use [`xhr.abort()`](https://api.jquery.com/jQuery.ajax/#advanced-options) once the [`xhr.readystate`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState) becomes 2, meaning the request was sent. But you would need to [check the progress](https://stackoverflow.com/a/20984502/1144627) of your request. – Will B. Jan 15 '19 at 17:03
  • Hey, I have PHP v5.4, it runs on a Linux server, I use PostreSQL in the script I uses the phpseclib library to SSH into a remote device and alter its config file. But I have just found a fatal flaw with what I'm trying to do. As before the script finishes, the device reboots, and breaks connection. Cant really go into much more detail as a NDA prevents that. I have decided that I am going to use CRON via Linux to schedule script runs instead of clients triggering script runs, Thanks for the Input! – Dagroa Jan 15 '19 at 17:43
  • Yea a cron job would have been my suggestion.I also strongly recommend prefixing your cron jobs with [`nice`](https://linux.die.net/man/1/nice) rather than just the desired command. Nice will allow you to limit the priority of the background command process, so that other processes can run without being impacted. I am working on a solution to your original question however, since I am curious. – Will B. Jan 15 '19 at 17:48

2 Answers2

1

You can setup what you call a background worker. The function executed will be done in the 'background', meaning users don't have to wait until the function fully finish. You can then expose a route/ Api to call that function. In laravel they call it Jobs that can be queued.

Personally, I don't have any experience setting up a background worker except in Laravel, which comes out of the box. But you can check these references!

Class Worker PHP

doBackground PHP

Laravel Queues

Adis Azhar
  • 1,022
  • 1
  • 18
  • 37
  • It's important to note that both [`Class Worker` (pThreads - last updated 2016)](http://pecl.php.net/package/pthreads) and [`doBackground` (Gearman - last updated 2013)](https://pecl.php.net/package/gearman) are PECL extensions that appear to no longer be maintained and may be difficult to install under some environments. – Will B. Jan 15 '19 at 16:03
  • Thank you @fyrye . Do you have any recommendations to what may be suitable? – Adis Azhar Jan 15 '19 at 16:24
  • It would depend on the environment the OP is working with. There are multiple alternatives including `cron tasks` and [`pcntl_fork`](https://secure.php.net/manual/en/function.pcntl-fork.php) to run PHP as background operations. I have used `pcntl_fork` to listen for UDP packets for days purely in PHP or to run report generations that took 20 minutes to complete. However due to the complexities of `pcntl_fork` and inherent issues with persistent connectivity, I personally switched to a more stable and better supported non-PHP solution for my requirements. – Will B. Jan 15 '19 at 16:38
0

I was curious and decided to attempt the xhr.abort() suggestion from my comment.

Below is just a basic example of accomplishing a long running PHP AJAX request, without the client waiting for the response from the server or rather early termination of the XHR and PHP continuing script execution.

This approach can be be adapted to run a local script that issues a cURL or SOAP request to an external host as well.

https://first.domain.com/send.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Send It</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
<form method="post" action="https://second.domain.com/recievingScript.php">
    <p><input type="text" name="name"/></p>
    <p>
        <button type="submit">Send</button>
    </p>
</form>
<script type="text/javascript">
    const writeLog = function(msg) {
        let date = new Date();
        window.console.log(date.toISOString() + ' ' + msg);
    };
    jQuery(function($) {
        let f = $('form');
        let xhrOptions = {
            url: f.attr('action'),
            type: f.attr('method'),
            data: {},
            cache: false,
            xhr: function() {
                let xhr = $.ajaxSettings.xhr();
                if ('undefined' === typeof xhr.upload.onload || null === xhr.upload.onload) {
                    //override the upload.onload event, only if it has not already been
                    xhr.upload.onload = function() {
                        //onload is triggered immediately after the POST headers are sent to the recipient
                        writeLog('Upload Completed - Ending Request');
                        xhr.abort();
                    };
                }
                return xhr;
            },
        };
        f.on('submit', function(e) {
            e.preventDefault();
            let formData = f.serialize();
            writeLog(formData);
            $.ajax($.extend(true, xhrOptions, {
                data: formData
            })).done(function(responseText) {
                //this is never triggered since the request is aborted
                writeLog('Success');
                writeLog(responseText);
            }).fail(function(jqxhr) {
                writeLog('Request ' + (jqxhr.readyState !== 4 ? 'Aborted' : 'Failed'));
            });
            return false;
        });
    });
</script>
</body>
</html>

Response:

send.html:18 2019-01-15T21:19:11.445Z name=Hello%20Dagroa
send.html:18 2019-01-15T21:19:11.558Z Upload Completed - Ending Request
send.html:18 2019-01-15T21:19:11.558Z Request Aborted

https://second.domain.com/recievingScript.php

\date_default_timezone_set('UTC');
\ignore_user_abort(true);
\header('Access-Control-Allow-Origin: https://first.domain.com');
if (!empty($_POST)) {
    $dateStart = new \DateTimeImmutable();
    //create a placeholder file
    if ($file = \tempnam(__DIR__, 'tmp_')) {
        $i = 1;
        //PHP >= 7 required for random_int - PHP < 7 use mt_rand() instead
        $rand = \random_int(5, 30);
        //add the number of seconds to create a stop date
        $dateStop = $dateStart->add(new \DateInterval(\sprintf('PT' . $rand . 'S')));
        while (new \DateTime() < $dateStop) {
            //loop until the stop date is reached
            $i++;
        }
        $dateEnd = new \DateTime();
        $dateDiff = $dateEnd->diff($dateStart);
        //write result to the temporary file
        \file_put_contents($file, \json_encode([
            'file' => \basename($file),
            'scriptFile' => \basename($_SERVER['SCRIPT_FILENAME']),
            'iterations' => $i,
            'start' => $dateStart->format(\DATE_RFC3339_EXTENDED),
            'end' => $dateEnd->format(\DATE_RFC3339_EXTENDED),
            'stop' => $dateStop->format(\DATE_RFC3339_EXTENDED),
            'elapsed' => $dateDiff->format('%i minutes, %s seconds, %f microseconds'),
            'slept' => $rand,
            'post' => $_POST,
        ], \JSON_PRETTY_PRINT));
    }
}

Response:

{
    "file": "tmp_hjLk4y",
    "scriptFile": "recievingScript.php",
    "iterations": 9653192,
    "start": "2019-01-15T21:19:12.171+00:00",
    "end": "2019-01-15T21:19:36.171+00:00",
    "stop": "2019-01-15T21:19:36.171+00:00",
    "elapsed": "0 minutes, 24 seconds, 3 microseconds",
    "slept": 24,
    "post": {
        "name": "Hello Dagroa"
    }
}

Notes: I use php-fpm 7.2 as an fcgi proxy using Apache 2.4, which should not matter other than with the random_int function call and DATE_RFC3339_EXTENDED.

Additionally the const and let usage in the JavaScript is an ECMAScript 6 specification, which is supported by all of the current major browser versions.

PHP output buffering should be disabled, otherwise you may need to force it to be flushed using flush() and/or ob_end_flush(). Certain other conditions from your web server configuration may also affect output buffering, such as gzip encoding.

jQuery will issue an OPTIONS request method just prior to the POST request method. Ensure to check that $_POST is not empty or check $_SERVER['REQUEST_METHOD'] for the desired type, to prevent double execution.

Will B.
  • 17,883
  • 4
  • 67
  • 69