2

While building a page to serve back to the user, I'd like to submit a background script to do some numerical analysis of data in the database and send the result to the user in an email. This process may take a minute or so, so I don't want to delay the page serve while it runs.

Is there a way to trigger another PHP script from the script that's building the page so it can send the page and be done while the other script runs in the background?

For testing, this TEST.PHP:

<?php
set_time_limit(0);
mail ('myemail@myemail.com','Test Email from ClockPie', 'foobar');
?>

Then I put this in the script that builds the page serve:

...
shell_exec ('test.php'); 
...

I'm running under Windoze 7 Home Premium. Is there something obviously wrong with this?

And yes, I know this is essentially a duplicate question and there are other existing questions about this same thing, but I'm too much of a peon here on StackOverflow to simply add comments and participate in the discussions :-(

Fredashay
  • 1,013
  • 3
  • 10
  • 16

5 Answers5

-1

shell_exec() is just like sitting at a command prompt and typing the string value and pressing return. You can't just type "test.php" and have it fire, you need to run PHP, and point it to your php file.

Depending on how you have php installed, you maybe able to simple change it to shell_exec('php test.php'); or you may have to provide a path to PHP like shell_exec('C:\php\bin\php.exe test.php');. The path will depend on your environment.

In the code you show, if you're just sending a single email, that shouldn't take more than a few seconds so I wouldn't even go down this route. But I don't think this is the end result of your code, and just a sample.

Steven V
  • 16,357
  • 3
  • 63
  • 76
  • The example in my question is just to test getting the process to trigger. The real code will run against the MySql database and take a minute or two to run. But yes, the email to the user is the end result. – Fredashay Aug 04 '13 at 02:52
-1

What you can do is output the page and flush() it so all data gets sent to the user. Then you sent the email. The user will see the page, but in the background it's still loading. This doesn't require anything like shell_exec();

This method is used by software like phpBB too to handle cron jobs taking quite some time.

flush() documentation: http://php.net/manual/en/function.flush.php

Basic Program Architecture:

  1. Build & Echo Page
  2. flush()
  3. Send Email
Jeroen
  • 15,257
  • 12
  • 59
  • 102
  • 2
    This doesn't work in all situations (e.g. if gzip compression is enabled) – Mike Aug 03 '13 at 20:19
  • 2
    Also depending on your server setup, `flush()` is not guaranteed to actually output the page to the browser, or can become very very complicated to deal with. http://serverfault.com/questions/488767/how-do-i-enable-phps-flush-with-nginxphp-fpm – Steven V Aug 03 '13 at 20:25
  • 1
    This sounds like the perfect solution. No need to deal with external processes. I wonder why your answer got a -1. I'll try it and see for myself... – Fredashay Aug 04 '13 at 02:56
  • I tried this. This looks like it might have worked because it sent the email, but I don't know the flush() caused the page to be sent before sending the email or if it simply ignored the flush(). – Fredashay Aug 04 '13 at 03:39
  • @Fredashay4 Add a sleep statement after the flush to see if it works in your setup. – Jeroen Aug 04 '13 at 07:45
  • 1
    The sleep caused the whole thing to hang for the duration of the sleep, so I guess the flush had no effect. – Fredashay Aug 04 '13 at 17:59
-1

The following code works on my system. Note that I'm just a hobbyist, not an expert, so this might not fall under the category of 'best practices', and I have no idea what the security implications might be, but this absolutely works, creating multiple threads that all run concurrently. Never mind about the folder name 'Calories'. That just happens to be the folder I was working in when I threw together this example code.

main.php:

error_log('Hello, world, from main!');

$numberOfThreadsToCreate = 3;

for($i = 0; $i < $numberOfThreadsToCreate; ++$i) {
    error_log("Main starting child {$i}");

    $fp = fsockopen('localhost', 8888);
    if(!$fp) {
        error_log("$errstr ($errno)");
        exit;
    }

    $firstSleep = $numberOfThreadsToCreate - $i;
    $header = "GET /Calories/thread.php?threadID={$i}&firstSleep={$firstSleep}"
                . " HTTP/1.1\r\n"
                . "Host: localhost\r\n"
                . "Connection: Close\r\n\r\n";

    $r = fputs($fp, $header); 
    fclose($fp);

    sleep(1);
}

for($i = 0; $i < 5; ++$i) {
    sleep(1);
    error_log('Main is still running');
}

error_log("Goodbye, cruel world, from main!");


thread.php

$myThreadID = $_GET['threadID'];
$sleep = $_GET['firstSleep'];

error_log("Hello, world, from child thread, ID={$myThreadID}!");
for($i = 0; $i < 5; ++$i) {
    error_log("Child {$myThreadID} sleeping for {$sleep} seconds");
    sleep($sleep);
    $sleep = 1;
}

error_log("Goodbye, cruel world, from child thread, ID={$myThreadID}!");

And the logfile results:

Hello, world, from main!
Main starting child 0
Hello, world, from child thread, ID=0!
Child 0 sleeping for 3 seconds
Main starting child 1
Hello, world, from child thread, ID=1!
Child 1 sleeping for 2 seconds
Main starting child 2
Hello, world, from child thread, ID=2!
Child 2 sleeping for 1 seconds
Child 1 sleeping for 1 seconds
Child 2 sleeping for 1 seconds
Child 0 sleeping for 1 seconds
Child 1 sleeping for 1 seconds
Child 2 sleeping for 1 seconds
Child 0 sleeping for 1 seconds
Main is still running
Child 1 sleeping for 1 seconds
Child 2 sleeping for 1 seconds
Child 0 sleeping for 1 seconds
Main is still running
Child 1 sleeping for 1 seconds
Child 2 sleeping for 1 seconds
Child 0 sleeping for 1 seconds
Main is still running
Goodbye, cruel world, from child thread, ID=1!
Goodbye, cruel world, from child thread, ID=2!
Main is still running
Goodbye, cruel world, from child thread, ID=0!
Main is still running
Goodbye, cruel world, from main!
SaganRitual
  • 3,143
  • 2
  • 24
  • 40
  • Problem is that the main script still has to wait for the child script to finish, so you have gained nothing. – Mike Aug 03 '13 at 20:29
  • @Mike Actually, no, I've had success with this method. It works great. – SaganRitual Aug 03 '13 at 20:33
  • So there's a dispute whether this will work or not. Hmmm. I'll try it myself and see.... – Fredashay Aug 04 '13 at 02:57
  • @Fredashay There's no dispute. It works great for me. I don't know what you're doing differently. If I recall correctly, I had to do some fiddling with PHP to enable sockets. But in the end, I got it working and now have a script that starts multiple, concurrently running threads. – SaganRitual Aug 04 '13 at 11:35
  • Okay, I'll try "fiddling" with the sockets later to see if there's an INI setting I need to change to make it work... – Fredashay Aug 04 '13 at 18:01
  • @GreatBigBore I forgot to say this the other day. I take back my original comment. I didn't notice the `Connection: Close` header. However you probably need to do `ignore_user_abort` or the script will stop executing as soon as the script receives this header, right? – Mike Aug 04 '13 at 21:26
  • @Mike I'm confused about what you mean. As you can see from the logfile, the scripts are running to completion with no problems. No aborts are occurring. – SaganRitual Aug 04 '13 at 21:31
  • @GreatBigBore the `Connection: Close` header aborts the request, which is what is allowing it to run in the background. If you get rid of this, you would have to wait until the child script finished, which is what my original comment was about. – Mike Aug 04 '13 at 21:57
-1

If you want to fork another PHP process, you would need to use pcntl_fork. However I don't think that this is the best method here. Instead I would make the script accessed by the browser add the data to be processed to a queue. Then set up a scheduled task to run every 'x' number of minutes that will process the queue and email the user when done.

I am assuming, of course, that you will make this scheduled script to be extremely lightweight if there is actually nothing in the queue (i.e. it should take a few milliseconds to complete). If not, you will be bogging down your server every few minutes and in that case looking into something like pcntl_fork would be the better solution.

One disadvantage of doing it this way is if you set it to run, for example, every 5 minutes, but it only takes 2 minutes to process the data, the user would be required to wait up to 3 extra minutes to receive the email. If this is a problem, tweak it to run more often.

Mike
  • 23,542
  • 14
  • 76
  • 87
  • Won't fork just create a duplicate of the same code? I also thought about doing it by writing each request as a row in a request table and then having a cron process run once an hour or so and fulfill all the user requests, but I don't know how much control I'll have when I place the finished site with a web hosting company such as GoDaddy. I'd rather not have to depend on the tech support people at the web host setting up crons for me. – Fredashay Aug 04 '13 at 03:00
  • The pcntl_fork() function creates a child process that differs from the parent process only in its PID and PPID. This doesn't mean you can't be creative and have the same code first check if something exists in the process queue and process it, or else add something to the process queue. – Mike Aug 04 '13 at 20:48
-1

You can use shell_exec() to run your 'sub php script' asynchronously, meaning that it will run in the background while your main php script continues, by appending the command that fires-off your 'sub php script' with the & symbol. So, it would be something like this:

$cmd="php /path/to/you/php/sub/script.php &";
shell_exec($cmd);
//main script continues running here, while sub script runs in the background...
mti2935
  • 11,465
  • 3
  • 29
  • 33