Es frecuente que en las aplicaciones existan procesos «pesados» y por lo tanto «lentos» que deseamos que los usuarios no los puedan ejecutar varias veces en un periodo corto de tiempo o que estos procesos se ejecuten de uno en uno, para no sobrecargar el servidor, y así seguir dando buen tiempo de respuesta al resto de procesos y usuarios.
También, nos puede surgir el mismo problema cuando queremos que un recurso de nuestro sistema sólo esté accedido por un único proceso, quedándose a la espera de ser liberado para el resto de procesos que requieran ese recurso.
Algunos ejemplos que pueden requerir esta gestión de proceso único, son:
- La recarga o copia de información desde otro sistema (proceso batch masivo).
- Copia de seguridad de datos o copia de respaldo de backup previos
- Elaboración de informes muy pesados. Procesos de cierre o reconfiguración del sistema.
- Utilización de un recurso del server (fichero, puerto serie, USB, certificado digital, etc.)
- Etc.
Objetivo
Disponer de una lógica, en los proyectos desarrollados en PHPRunner, para controlar:
- Que un proceso específico sólo este en ejecución una única vez y el resto de peticiones que concurran las rechace el sistema.
- Que un proceso específico sólo este en ejecución una única vez y que el resto de peticiones concurrentes se encolen y se liberen según el orden de llegada (cola FIFO).
DEMO: https://fhumanes.com/single_execution/
Solución Técnica
El control de la concurrencia se puede realizar al menos con estos 3 medios.
- A través de un «semáforo» del sistema operativo
- A través del bloqueo de un fichero.
- A través de una tabla del gestor de base de datos.
En este ejemplo se ha utilizado el control a través del bloqueo de un fichero (ficheros, para disponer de varios controles).
Esta es la imagen de la aplicación:
(1) – Botón de ejecución del proceso y rechazado de las peticiones que concurran en la ejecución.
(2) – Botón de ejecución del proceso y encolamiento de las peticiones hasta que no se termine la anterior
El proceso es muy simple, retarda el proceso 15 segundos:
<?php // time initial $time_initial = date('h:i:s'); // sleep 15 seg sleep(15); // Time final $time_final = date('h:i:s'); ?>
La función que realiza el control de un proceso y del resto, su rechazo, es:
<?php function single_execution( $semaphore, $process ) { $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 Proccess include ($process); fflush($fp); // flush output before releasing the lock flock($fp, LOCK_UN); // release the lock return array(true, "Process executed correctly"); } }
La función que realiza la cola FIFO es:
<?php function fifo_execution( $semaphore, $process ) { $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)) { // acquire an exclusive lock ftruncate($fp, 0); // truncate file // execute Proccess include ($process); fflush($fp); // flush output before releasing the lock flock($fp, LOCK_UN); // release the lock } fclose($fp); return array(true, "Process executed correctly"); }
La programación de los botones de 3 estados es:
- Previo (Javascript)
$('a[id^="Process_unique"]').attr("disabled", true); // Deshabilitar botón
- Server (PHP)
include ("MyCode/semaphore_code.php"); $status = single_execution("semaphore_1", __DIR__."/MyCode/slow_processes/process_1.php"); $result['status'] = $status[0]; $result['message'] = $status[1];
- Después (JavaScript)
$('a[id^="Process_unique"]').removeAttr("disabled") // Habilitar botón var message = 'Status = '+ result["status"]+' '+result["message"]; ajax.setMessage(message);
Para testear la solución deberá disponer de varios navegadores en un único PC o varios navegadores en distintos equipos y cliquear los botones lo más concurrente posible.
Como en los demás ejemplos, para cualquier duda contactar conmigo a través de mi email [email protected] .
También os dejo el proyecto para que lo descarguéis y probéis en vuestros PC’s.