0

I saw some answers explaining how to run multiple PHP scripts synchronously from one PHP file with exec function. For example :

<?php
exec("script1.php");
exec("script2.php");
?>

But this is not what I want. My problem is that these scripts are doing intensive SQL requests so if I do that the calling PHP script will die with timeout. Increasing the timeout is not a solution. I don't want to execute these scripts asynchronously in order to avoid to overload the MySQL database at the same time.

So I was thinking to do it in Javascript or maybe with a cron job. In JS I would like to execute script2.php only if script1.php is finished and so on. With JQuery I could do something like that:

<script>
myfunction();

function myfunction()
{
   promise = longfunctionfirst().then(shortfunctionsecond);
}
function longfunctionfirst()
{
   d = new $.Deferred();
fetch("script1.php", {})
    .then( async response => await response.json())
    .then(function(data) {
        //Process data
    });
   setTimeout('alert("first function finished");d.resolve()',3000);
   return d.promise()
}
function shortfunctionsecond()
{
   d = new $.Deferred();
fetch("script2.php", {})
    .then( async response => await response.json())
    .then(function(data) {
        //Process data
    });
   setTimeout('alert("second function finished");d.resolve()',200);
   return d.promise()
}
</script>

It could be ok if I have only 2 scripts but I have more than 10.

Do you think I can find an effective solution with JS, jQuery or with cron jobs?

vegmaster
  • 37
  • 6
  • 2
    cli scripts dont have timeouts – Lawrence Cherone Apr 20 '22 at 15:28
  • `It could be ok if I have only 2 scripts but I have more than 10`...why would 10 be a problem? If you have a list in JS of the scripts to run, you can write the code in a more dynamic way so it launches as many requests as there are scripts in the list (but it can still do them sequentially). Then it would not matter if you have 1 script or 1 million. – ADyson Apr 20 '22 at 15:35
  • 1
    Another option is to hand off the execution to a background script e.g. triggered regularly by a cron job, which collects any requests for the scripts to be run (e.g. as stored in a DB table) and runs them, then logs when it's finished. You can have a status page in your site if you wish which shows you the status of any requests. – ADyson Apr 20 '22 at 16:04
  • If you want the scripts to be run _consecutively_, you could simply have a "queue handler script" that holds a map of the script calls; is called back by each "actual" script when finished; and is responsible for calling "next" on receiving a "finished" prompt; taking a nap in between each call if you don't want to hammer the server; and logging the responses if needed. Make sense? – Markus AO Apr 20 '22 at 17:45
  • 1
    IMHO the ideal solution is to use a queue/worker model. The job is submitted to a queue, eg: RabbitMQ, and there are one or more workers attached to the queue waiting for work. Work happens on-demand without waiting for cron to trigger, you're not spinning up virtually limitless numbers of processes on-demand or holding open web workers, you can scale the number of workers to meet demand, and it make it easier to scale _out_ to multiple servers if need be. – Sammitch Apr 20 '22 at 23:37
  • @LawrenceCherone Are you sure that cli scripts don't have timeout limit? It seems it is limited to 300s: https://forums.cpanel.net/threads/php-cli-has-maximum-execution-time-clamped-at-300-seconds.676549/ – vegmaster Apr 22 '22 at 09:19

2 Answers2

1

exec runs that command inside a shell and it's not very smooth eating resources and adding security concerns.

The Javascript strategy you are chasing it's just half of the story. That's just the client side invoking different endpoints in sequence waiting for the previous one to have finished responding.

Those endpoints will need to be resolved by your server side application and those requests will still be subject to timeout.

Since those scripts are supposed to take long time and are not suitable to run inside the span of an HTTP request, there's no other way but run them async in respsect to the response that will be sent to the request initiator.

I'm not telling you to run 1,2,3 out of sequence... I'm just saying that even one of them takes too long and that's what requires to be detached from the response.

So one of the many solutions you could approach, it's one suggested here on SO:

    ignore_user_abort(true);//not required
    set_time_limit(0);
    
    ob_start();
    // do initial processing here
    echo $response; // send the response
    header('Connection: close');
    header('Content-Length: '.ob_get_length());
    ob_end_flush();
    @ob_flush();
    flush();
    fastcgi_finish_request();//required for PHP-FPM (PHP > 5.3.3)
    
    // now the request is sent to the browser, but the script is still running
    // so, you can continue...
    
    // In particular your calls to the functions entry point
    // in your scripts still respecting their sequence.
    // Before the sequence ends, you should write the status somewhere 
    // (like in a file or in the DB) so that you'll have another endpoint
    // that will check that status and will tell you 
    // if the process finished or still running.


    die(); //a must especially if set_time_limit=0 is used and the task ends

But yet that's not exactly what you asked. You still insisted to run those fetch commands in sequence and maybe it shouldn't be my business to push you toward a different approach. I tried fixing your js code so that it will run correctly how you expected. Unfortunately doing fetches like that is very unfriendly unless you rely on your own cors-proxy and I couldn't craft a working example here on the snippet.

I can try to explain a working approach though. If you factor that function running the fetch in a way that it will be async and general (you will call it with await if you can't easily deal with asyncronous events), when the general success callback will be invoked, it will need to know which is the url fetched and where it belongs to the list. To make sure such callback will see those information, you could for example put them in an object in the global scope. To declare such a variable you can do it from any scope without using let/const/var before the assignment to the variable.

I will be heavily downvoted for giving such an advice but it's the easiest way to communicate a solution without having the chance to test my code.

The object will just contain: a list of scripts to run with their urls, the cursor where you are staying processing your i-th url in the list and, in case you want, a different callback for each script.

Then you just call with await your function passing it the list and the cursor set to zero. Now everytime you get into the success callback, it will fetch the url at the cursor position, process its data, increment the cursor and call again that function until the cursor got equal to the length of the urls list.

This was a very informal solution and not very detailed with examples. I hope you've got the point of my suggestions and you can get somewhere with your problems.

Diego D
  • 6,156
  • 2
  • 17
  • 30
1

Finally I created a batch script with a loop (which is equivalent to cron on windows)

@echo off 
setlocal enabledelayedexpansion 
set User[0]=user1
set User[1]=user2
set User[2]=user3
for /l %%x in (0,1,2) do (
    if defined User[%%x] (
curl -s -o NUL "http://localhost/testsite/script?user=!User[%%x]!"
    )
)
endlocal
vegmaster
  • 37
  • 6