DJJob: Trabajos en background para PHP

Hace tiempo que vamos encontrándonos de forma recurrente con este problema… ¿cómo conseguir que PHP realice un trabajo en background e ir consultando cuánto le falta o cuando ha terminado dicho trabajo?

La pista ya la puso sobre la mesa Delayed::Job, una gema de Ruby on Rails, que crea una tabla en la base de datos, y se encarga de ir actualizando dicha tabla de modo que, en el momento que se lanza el proceso (a través de un cron, por ejemplo), ejecuta todas las tareas que estén esperando en la cola. Realmente es una tabla en la que se inserta cada tarea, y después una ejecución aparte que se encarga de lanzarla.

En PHP, con DJJob, se consigue el mismo resultado. Se crea la tabla para la gestión de los trabajos, y el sistema tiene mecanismos que permiten encolar trabajos y sacarlos para su ejecución, así como preguntar si está en ejecución o terminado. Si adaptamos un poco la tabla, podríamos incluso agregar un campo que nos diese el porcentaje de compleción de la tarea.

Probamos la teoría

Vamos a hacer un simple ejemplo, descargamos el fichero DJJob.php y creamos otros dos ficheros, el example.php:

 
<?php
 
include(__DIR__ . "/DJJob.php");
 
DJJob::configure("mysql:host=localhost;dbname=jobs", array ( 'mysql_user' => 'root', 'mysql_pass' => 'root'));
 
class HelloWorldJob {
        public function perform() {
                echo "Hello world!\n";
        }
}
 
DJJob::enqueue(new HelloWorldJob());

Y el worker.php, que será el que se encargue de sacar el trabajo y ejecutarlo:

 
<?php
 
include(__DIR__ . "/DJJob.php");
 
DJJob::configure("mysql:host=localhost;dbname=jobs", array ( 'mysql_user' => 'root', 'mysql_pass' => 'root'));
 
class HelloWorldJob {
        public function perform() {
                echo "Hello world!\n";
        }
}
 
$worker = new DJWorker(array ( "count" => 1, "sleep" =>  ));
$worker->start();

Antes de ejecutarlo, necesitaremos tener en la máquina local una base de datos MySQL con este script cargado: jobs.sql; y con acceso root y clave root (se puede cambiar, modificando el código de ambos códigos PHP).

Una vez lo tengamos, solo nos quedará ejecutarlo:

$ php example.php
$ php worker.php
* [JOB] Starting worker host::bosqueviejo pid::18835 on queue::default
* [JOB] attempting to acquire lock for job::1 on host::bosqueviejo pid::18835
Hello world!
* [JOB] completed job::1
* [JOB] worker shutting down after running 1 jobs, over 1 polling iterations

Las opciones del worker, hacen que la ejecución sea solo una, por lo que se puede agregar la ejecución a un cron. No obstante, si no se pasan opciones, la ejecución se hace recurrentemente cada 5 segundos, lo cual es más óptimo si se emplea desde la web. Además, el sistema lanza varios hijos (en caso de tener pcntl_fork) con lo que, trabajará de forma paralela en caso de que haya varias tareas en la cola.

Una buena idea a tener en cuenta si queremos desarrollar cargas de ficheros que tengan un procesamiento bastante extenso, o trabajos de conexión y comunicación con otros servidores (como por ejemplo pagos) que requieran de mucho tiempo y puedan terminar en una desconexión por tiempo de espera agotado, o paciencia del usuario agotada.