En cuanto tienes una aplicación con un mínimo de complejidad, enseguida identificas que necesitas ejecutar tareas en segundo plano o batch, para ejecutar procesos a determinadas horas, por ejemplo, migración de información, backup de tu sistema, etc.
Si dispones de la totalidad de acceso a tu sistema, enseguida te acuerdas de CRON o Crontab, que es el planificador de tareas del sistema operativo.
No siempre tienes total acceso para este tipo de configuración y además es complejo. Además, tienes el problema de que pasar tu aplicación de una máquina a otra, tienes que recordar que hay acciones fuera de tu aplicativo, por lo que es una posible fuente de problemas.
He diseñado un sistema que permite integrarse 100% con PHPrunner y que elimina el problema de migración de configuración, y facilita la definición y gestión de estas tareas Batch.
Objetivo
Disponer de un sistema, similar en funcionalidad al Crontab y totalmente integrado con PHPRunner.
Este sistema debe permitir:
- Definir las tareas en formularios hechos en PHPRunner, en donde se especifique:
- Fecha inicial de ejecución de la Tarea.
- Fecha final de ejecución de la Tarea.
- Intervalo de ejecución, definido en meses, días, horas y minutos.
- Comando de ejecución con path variable dependiendo de la ubicación del aplicativo.
- Registro por cada ejecución que indique: hora de inicio, hora de final y resultado de la ejecución.
- Definición de tiempo máximo de ejecución y si se supera, cancelación del proceso.
- Ejecución única en el sistema de una tarea Batch (para no sobrecargar el sistema).
- En las tareas Batch, posibilidad de trazas, para depuración y control, integrada con la aplicación de PHPRunner.
- Normalización de las tareas Batch, para utilizar la conexión de base de datos del aplicativo de PHPRunner (facilidad en la migración entre sistemas).
DEMO: https://fhumanes.com/scheduler/
(Esta versión está limitada para impedir mal uso de los ejemplos)
Información que tratamos para cada tarea. No se ejecuta cuando está marcada el campo de «Status finished» o le campo «Date Next Execution» es inferior a la fecha actual.
También, en la definición del comando a ejecutar hay estas sustituciones:
{DIR} .- Se sustituye por el path donde se este ejecutando el aplicativo
{ID_TASK} .- Se sustituye por el ID del registro de log de ejecución.
Solución técnica
En el ejemplo se han utilizado funcionalidades descritas en los artículos:
1/ Método sencillo para depurar programas.
2/ Método de ejecución única de código.
En el caso de 1/ es para poder producir trazas tanto en nuestro aplicativo como en los procesos Batch que se ejecuten. En el caso 2/ es para que sólo se ejecute un Batch en un momento. Esto es para no recargar al servidor y asegurarnos que en el momento de análisis y tratamiento de los procesos Batch no haya concurrencia de acceso a esa información.
El sistema es muy simple, hay una tabla donde se registra todas las tareas Batch y sus datos de fechas y frecuencia y una tabla dependiente de esta que informa de las veces que se ha ejecutado y el resultado de la ejecución.
En el ejemplo, se ha incluido el control de los procesos batch en el evento «After application initialized»
// To debug PHP code on the server function custom_error($number,$text){ // Function to produce the error file global $debugCode; if ($debugCode == true ) { $ddf = fopen(__DIR__ .'/../error.log','a'); fwrite($ddf,"[".date("r")."] Error $number: $text\r\n"); fclose($ddf); } } $debugCode = true; // Change to "false" to eliminate traces // custom_error(1,"URL ejecutada: ".$_SERVER["REQUEST_URI"]); // To debug require_once(__DIR__."/../scheduler_job_batch.php"); // Scheduler Jobs Task Batch
Un método alternativo y mejor es ejecutar este fichero «scheduler_job_batch.php», desde el CRONTAB cada 1 minuto. En el caso de que esto no sea posible, tal y como está en el ejemplo funciona, porque siempre que haya una petición va a verificar si existe un Job Batch pendiente de ejecutar.
El fichero «scheduler_job_batch.php», tiene este código:
<?php @ini_set("display_errors","1"); @ini_set("display_startup_errors","1"); require_once("include/dbcommon.php"); // To ensure that only one process is validating if you have to run a batch function single_execution( $semaphore, $process ) { $dir_path_base = __DIR__; $path_semaphore = __DIR__."/semaphores/".$semaphore.".lock"; $fp = fopen($path_semaphore, "r+"); if ($fp == false ) { // file not found return array(false ,"The semaphore file does not exist!"); } if (!flock($fp, LOCK_EX|LOCK_NB, $blocked)) { if ($blocked) { // another process holds the lock return array(false, "Couldn't get the lock! Other script in run!"); } else { // couldn't lock for another reason, e.g. no such file return array(false , "Error! Nothing done."); } } else { // lock obtained ftruncate($fp, 0); // truncate file // execute Process include ($process); fflush($fp); // flush output before releasing the lock flock($fp, LOCK_UN); // release the lock return array(true, "Process executed correctly"); } } $status_arr = single_execution( 'scheduler', 'My_Code/scheduler_process.php'); // Execute Process Scheduler single custom_error(20,"Control Scheduler: ".$status_arr[1]); // To debug
En este código se utiliza la función de ejecución única (sólo un proceso Batch en funcionamiento). Mientras ocurre esto, las peticiones no esperan, seguirán funcionando sin problema.
El proceso de control de las Tareas Batch se hace en este fichero «MyCode/scheduler_process.php»:
<?php /*execute program and write all output to $out terminate program if it runs more than XXX seconds */ function execute($cmd, $stdin=null, &$stdout, &$stderr, $timeout=false) { $pipes = array(); $process = proc_open( $cmd, array(array('pipe','r'),array('pipe','w'),array('pipe','w')), $pipes ); $start = time(); $stdout = ''; $stderr = ''; if(is_resource($process)) { stream_set_blocking($pipes[0], 0); stream_set_blocking($pipes[1], 0); stream_set_blocking($pipes[2], 0); fwrite($pipes[0], $stdin); fclose($pipes[0]); } while(is_resource($process)) { //echo "."; $stdout .= stream_get_contents($pipes[1]); $stderr .= stream_get_contents($pipes[2]); if($timeout !== false && time() - $start > $timeout) { proc_terminate($process, 9); return 1; } $status = proc_get_status($process); if(!$status['running']) { fclose($pipes[1]); fclose($pipes[2]); proc_close($process); return $status['exitcode']; } usleep(1000000); } return 1; } // Main code $rs = DB::Query( "SELECT id_task, title, date_init, date_next, date_end, status, increment_minutes, increment_hours, increment_days, increment_months, command, max_time_minutes FROM scheduler_task where status = 0 and date_next <= now()" ); while( $data = $rs->fetchAssoc() ) { custom_error(10,"Id Task scheduler: ".$data['id_task']); // To debug // Insert record of Execution $data2 = array(); $data2["date_init"] = now(); $data2["scheduler_task_id"] = $data['id_task']; DB::Insert("scheduler_task_execution", $data2 ); // get the ID of the inserted record $scheduler_task_execution = DB::LastId(); // Plan next execution $data3 = array(); $keyvalues3 = array(); // $time = new DateTime($data['date_next']); $time = new DateTime(); // $interval = new DateInterval('P2Y4DT6H8M'); $time->add(new DateInterval('P'. $data['increment_months'].'M'. $data['increment_days'].'D'. 'T'. $data['increment_hours'].'H'. $data['increment_minutes'].'M')); $stamp = $time->format('Y-m-d H:i'); $data3['date_next'] = $stamp; if ( $data['date_end'] < $stamp ) { // Ha traspasado fecha de fin $data3['status'] = 1; } $keyvalues3['id_task'] = $data['id_task']; DB::Update("scheduler_task", $data3, $keyvalues3 ); // Launch independent process // $dir_path_base = __DIR__; // In process father $command = str_replace("{DIR}", $dir_path_base, $data['command']); $command = str_replace("{ID_TASK}", $scheduler_task_execution, $command); custom_error(11,"Execute Command: ".$command ); // To debug $max_time = $data['max_time_minutes']*60; execute($command, null, $out, $err, $max_time); // $result = shell_exec($command); // $result = exec($command); custom_error(12,"Execute Command -Result: ".$out ); // To debug custom_error(13,"Execute Command -Error: ".$err ); // To debug /* // Update "Execution" $data2 = array(); $keyvalues2 = array(); $data2["date_end"] = now(); $data2["result"] = $result; $keyvalues2["id_task_execution"] = $scheduler_task_execution; DB::Update("scheduler_task_execution", $data2, $keyvalues2 ); */ }
Tiene una función especial para la ejecución de los procesos Batch «execute», que además controla las salidas «stroutput» y «strerror», además de controlar el tiempo máximo de ejecución que hayamos especificado a la tarea Batch.
El ejemplo de la tarea Batch es simple, pero muestra las características de este tipo de código:
<?php @ini_set("display_errors","1"); @ini_set("display_startup_errors","1"); require_once(__DIR__."/../../include/dbcommon.php"); custom_error(50,"Job Bacht Test1 in execution"); // To debug // GET Parameter line Command $param = getopt(null, ["id_task:"]); custom_error(52,"Job Bacht Param: ".$param['id_task']); // To debug // time initial $time_initial = date('h:i:s'); // sleep 5 segundos sleep(5); // Time final $time_final = date('h:i:s'); custom_error(51,"Time Initial: ".$time_initial." Time End :".$time_final); // To debug echo "ok"; // Update "Execution" $data2 = array(); $keyvalues2 = array(); $data2["date_end"] = now(); $data2["result"] = 'ok'; $keyvalues2["id_task_execution"] = $param['id_task']; DB::Update("scheduler_task_execution", $data2, $keyvalues2 ); custom_error(60,"End Job test1.php"); // To debug
Siempre se le pasa el parámetro «id_task», que es el ID del registro de LOG de ejecución, para que cierre/informe del resultado del proceso.
Podemos ver que recoge el entorno de ejecución PHPRunner, conexión y sintaxis de acceso a la base de datos y también se puede ejecutar el sistema de trazas definido y activado o no, en el evento «After application initialized».
Yo he disfrutado mucho haciendo este código y probándolo. Espero que a vosotros también os guste.
Para cualquier duda o lo que necesitéis, contactar conmigo a través de mi email [email protected]
Os facilito los fuentes para que lo podáis instalar en vuestros PC’s, y podáis hacer los ajustes que necesitéis.