To follow long-running tasks is common but not really easy to implement the first time. Here is a complete example.
(a sample of long-running task in a Sellermania's product)
Context
The task
Imagine you currently have the following task, and want to display a progress bar to your visitors.
PHP task.php
<?php
$total_stuffs = 200;
$current_stuff = 0;
while ($current_stuff < $total_stuffs) {
$progress = round($current_stuff * 100 / $total_stuffs, 2);
// ... some stuff
sleep(1);
$current_stuff++;
}
The UI
Your beautiful UI looks like this:
HTML ui.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title>My Task!</title>
</head>
<body>
<a id="run_task" href="#">Run task</a>
<div id="task_progress">Progression: <span id="task_progress_pct">XX</span>%
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script type="text/javascript">
$('#run_task').click(function(e) {
e.preventDefault();
$.get('task-launch.php');
});
</script>
</body>
</html>
The launcher
We need to launch the task in background, in order to make this demonstration relevant. So, this php script will be called asynchronously when clicking "run", and will execute the task above in background.
PHP task-launch.php
<?php
// launches the task in background
// see https://mcmap.net/q/358111/-multi-threading-in-php for details about the arguments
exec("/usr/bin/php task.php > /dev/null 2>&1 &");
Problems
There are 3 problems here:
you can run the task more than once by clicking several times the run button, how to avoid several tasks in the background at the same time?
the task is ran at server side, so we need to do something to access the server and ask for progression information.
and when we will be connected to the server side, the $progress
variable is unavailable for reading, because it is stored within the context of the task.php
running instance.
Solutions
Store progression information into something readable from outside
Usually, progression information are stored into a database, or a file, or whatever that can be writtable by a program (your task actaully), and readable by another (your ui where progression should be displayed).
I developped a class for sharing data within several php applications (available on github here), it works about the same way as stdClass
but always securely synchronize its contents into a file.
Just download src/Sync.php
and change the task.php above by:
PHP task.php
<?php
require("Sync.php");
$total_stuffs = 200;
$current_stuff = 0;
$shared = new Sync("some_file.txt");
$shared->progress = 0;
while ($current_stuff < $total_stuffs) {
$shared->progress = round($current_stuff * 100 / $total_stuffs, 2);
// ... some stuff
sleep(1);
$current_stuff++;
}
// to tell the follower that the task ended
$shared->progress = null;
Important note: here, some_file.txt
is where are stored your task's shared data, so don't hesitate to use "task_[user_id].txt" for example if each user has its own task. And look at the readme on github to optimize file access.
Use the synchronized variable to protect the task launcher
- The progression is set to 0 at the beginning of the task, so the first thing to do is to check, before running the task, that this progression is not set to 0.
PHP task-launch.php
<?php
require("Sync.php");
$shared = new Sync("some_file.txt");
if (is_null($shared->progress)) {
exec("/usr/bin/php task.php > /dev/null 2>&1 &");
}
- If the run button is clicked twice very quickly, we can still have 2 instances of the task. To handle this case, we need to simulate a mutex, in a word, make the variable only available to the current application to do some stuff - other applications will stay sleeping until the shared variable is unlocked.
PHP task.php
<?php
require("Sync.php");
$total_stuffs = 200;
$current_stuff = 0;
$shared = new Sync("some_file.txt");
// here is the magic: impossible to set the progression to 0 if an instance is running
// ------------------
$shared->lock();
if (!is_null($shared->progress))
{
$shared->unlock();
exit ;
}
$shared->progress = 0;
$shared->unlock();
// ------------------
while ($current_stuff < $total_stuffs) {
$shared->progress = round($current_stuff * 100 / $total_stuffs, 2);
// ... some stuff
sleep(1);
$current_stuff++;
}
// the task ended, no more progression
$shared->progress = null;
Warning: if your task crashes and never reach the end, you'll never be able to launch it anymore. To avoid such cases, you can also store the child's getmypid()
and some time()
stuffs inside your shared variable, and add a timeout logic in your task.
Use polling to ask the server progression information
Polling stands for asking for progression information to the server every lapse of time (such as, 1 sec, 5 secs or whatever). In a word, client asks progression information to the server every N secs.
- at server-side, we need to code the handler to answer the progression information.
PHP task-follow.php
<?php
require("Sync.php");
$shared = new Sync("some_file.txt");
if ($shared->progress !== null) {
echo $shared->progress;
} else {
echo "--"; // invalid value that will break polling
}
- at client-side, we need to code the "asking progression information to the server" business
HTML ui-polling.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title>My Task!</title>
</head>
<body>
<a id="run_task" href="#">Run task</a>
<div id="task_progress">Progression: <span id="task_progress_pct">XX</span>%
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script type="text/javascript">
$('#run_task').click(function(e) {
e.preventDefault();
<!-- not a good practice to run a long-running task this way but that's a sample -->
$.get('task-launch.php');
<!-- launches the polling business -->
setTimeout(function() {
getProgressionInformation();
}, 1000);
});
function getProgressionInformation() {
$.get('task-follow.php', function(progress) {
$('#task_progress_pct').html(progress);
if (progress !== '--') {
<!-- if the task has not finished, we restart the request after a 1000ms delay -->
setTimeout(function() {
getProgressionInformation();
}, 1000);
}
});
}
/* the task might be already running when the page loads */
$(document).ready(function() {
getProgressionInformation();
});
</script>
</body>
</html>
With a minimum of JavaScript ?
I also developed a jquery plugin, domajax, intended to do "ajax without javascript" (in fact, the plugin itself is in jQuery, but using it do not require JavaScript code), and by combining options you can do polling.
In our demonstration:
PHP task-follow.php
<?php
require("Sync.php");
$shared = new Sync("some_file.txt");
if ($shared->progress !== null) {
echo $shared->progress;
}
HTML ui-domajax.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title>My Task!</title>
</head>
<body>
<a
href="#"
id="run-task"
class="domajax click"
data-endpoint="task-launch.php"
data-domajax-complete="#polling"
>Run task</a>
<div
id="polling"
data-endpoint="task-follow.php"
data-delay="1000"
data-output-not-empty="#task-progress-pct"
data-domajax-not-empty=""
></div>
<div id="task-progress">Progression: <span id="task-progress-pct">--</span>%
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="//domajax.com/js/domajax/jquery.domajax.js"></script>
</body>
</html>
As you can see, there is no visible javascript at all in this code. Clean isn't it?
Other examples are available on domajax's website, look at the "Manage progress bars easily" tab in the demo pane. All options are heavily documented for details.