Cancelacion
Un navegador envio una solicitud, pero luego el usuario cerro la pagina. El servidor continua trabajando en una solicitud que ya no es necesaria. Seria bueno abortar la operacion para evitar costos innecesarios. O supongamos que hay un proceso largo de copia de datos que necesita ser cancelado repentinamente. Hay muchos escenarios en los que necesitas detener operaciones. Normalmente este problema se resuelve con variables de bandera o tokens de cancelacion, lo cual es bastante laborioso. El codigo debe saber que podria ser cancelado, debe planificar puntos de control de cancelacion y manejar correctamente estas situaciones.
Cancelable por Diseno
La mayor parte del tiempo, una aplicacion esta ocupada leyendo datos
de bases de datos, archivos o la red. Interrumpir una lectura es seguro.
Por lo tanto, en TrueAsync se aplica el siguiente principio: una corrutina puede ser cancelada en cualquier momento desde un estado de espera.
Este enfoque reduce la cantidad de codigo, ya que en la mayoria de los casos, el programador no necesita preocuparse
por la cancelacion.
Como Funciona la Cancelacion
Se utiliza una excepcion especial – Cancellation – para cancelar una corrutina.
La excepcion Cancellation o una derivada se lanza en un punto de suspension (suspend(), await(), delay()).
La ejecucion tambien puede ser interrumpida durante operaciones de E/S o cualquier otra operacion bloqueante.
$coroutine = spawn(function() {
echo "Iniciando trabajo\n";
suspend(); // Aqui la corrutina recibira Cancellation
echo "Esto no sucedera\n";
});
$coroutine->cancel();
try {
await($coroutine);
} catch (\Cancellation $e) {
echo "Corrutina cancelada\n";
throw $e;
}
La Cancelacion No Puede Ser Suprimida
Cancellation es una excepcion de nivel base, al mismo nivel que Error y Exception.
La construccion catch (Exception $e) no la capturara.
Capturar Cancellation y continuar trabajando es un error.
Puedes usar catch Async\AsyncCancellation para manejar situaciones especiales,
pero debes asegurarte de relanzar correctamente la excepcion.
En general, se recomienda usar finally para la limpieza garantizada de recursos:
spawn(function() {
$connection = connectToDatabase();
try {
processData($connection);
} finally {
$connection->close();
}
});
Tres Escenarios de Cancelacion
El comportamiento de cancel() depende del estado de la corrutina:
La corrutina aun no ha comenzado – nunca se iniciara.
$coroutine = spawn(function() {
echo "No se ejecutara\n";
});
$coroutine->cancel();
La corrutina esta en estado de espera – se despertara con una excepcion Cancellation.
$coroutine = spawn(function() {
echo "Trabajo iniciado\n";
suspend(); // Aqui recibira Cancellation
echo "No se ejecutara\n";
});
suspend();
$coroutine->cancel();
La corrutina ya ha terminado – no pasa nada.
$coroutine = spawn(function() {
return 42;
});
await($coroutine);
$coroutine->cancel(); // No es un error, pero no tiene efecto
Secciones Criticas: protect()
No toda operacion puede ser interrumpida de forma segura. Si una corrutina ha debitado dinero de una cuenta pero aun no ha acreditado otra – la cancelacion en este punto llevaria a la perdida de datos.
La funcion protect() aplaza la cancelacion hasta que la seccion critica se complete:
use Async\protect;
use Async\spawn;
$coroutine = spawn(function() {
protect(function() {
$db->query("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
suspend();
$db->query("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
});
// La cancelacion tomara efecto aqui -- despues de salir de protect()
});
suspend();
$coroutine->cancel();
Dentro de protect(), la corrutina se marca como protegida.
Si cancel() llega en este momento, la cancelacion se guarda
pero no se aplica. Tan pronto como protect() se completa –
la cancelacion diferida toma efecto inmediatamente.
Cancelacion en Cascada via Scope
Cuando un Scope es cancelado, todas sus corrutinas y todos los scopes hijos son cancelados.
La cascada va solo de arriba hacia abajo – cancelar un scope hijo no afecta al padre ni a los scopes hermanos.
Aislamiento: Cancelar un Hijo No Afecta a Otros
$parent = new Async\Scope();
$child1 = Async\Scope::inherit($parent);
$child2 = Async\Scope::inherit($parent);
// Cancelar solo child1
$child1->cancel();
$parent->isCancelled(); // false -- el padre no se ve afectado
$child1->isCancelled(); // true
$child2->isCancelled(); // false -- el scope hermano no se ve afectado
Cascada Descendente: Cancelar un Padre Cancela Todos los Descendientes
$parent = new Async\Scope();
$child1 = Async\Scope::inherit($parent);
$child2 = Async\Scope::inherit($parent);
$parent->cancel(); // Cascada: cancela tanto child1 como child2
$parent->isCancelled(); // true
$child1->isCancelled(); // true
$child2->isCancelled(); // true
Una Corrutina Puede Cancelar Su Propio Scope
Una corrutina puede iniciar la cancelacion del scope en el que se ejecuta. El codigo antes del punto de suspension mas cercano continuara ejecutandose:
$scope = new Async\Scope();
$scope->spawn(function() use ($scope) {
echo "Iniciando\n";
$scope->cancel();
echo "Esto aun se ejecutara\n";
suspend();
echo "Pero esto no\n";
});
Despues de la cancelacion, el scope se cierra – ya no es posible lanzar una nueva corrutina en el.
Tiempos de Espera
Un caso especial de cancelacion es un tiempo de espera. La funcion timeout() crea un limite de tiempo:
$coroutine = spawn(function() {
return file_get_contents('https://slow-api.example.com/data');
});
try {
$result = await($coroutine, timeout(5000));
} catch (Async\OperationCanceledException $e) {
// $e->getPrevious() contiene TimeoutException
echo "La API no respondio en 5 segundos\n";
}
Cuando se activa un token de cancelación (incluido el timeout), se lanza OperationCanceledException. La excepción original del token está disponible a través de $e->getPrevious(). Esto permite distinguir la activación del token de un error del propio objeto awaitable.
Verificacion del Estado
Una corrutina proporciona dos metodos para verificar la cancelacion:
isCancellationRequested()– la cancelacion fue solicitada pero aun no se ha aplicadoisCancelled()– la corrutina realmente se ha detenido
$coroutine = spawn(function() {
suspend();
});
$coroutine->cancel();
$coroutine->isCancellationRequested(); // true
$coroutine->isCancelled(); // false -- aun no procesado
suspend();
$coroutine->isCancelled(); // true
Ejemplo: Worker de Cola con Apagado Graceful
class QueueWorker {
private Async\Scope $scope;
public function __construct() {
$this->scope = new Async\Scope();
$this->queue = new Async\Channel();
}
public function start(): void {
$this->scope->spawn(function() {
while (true) {
$job = $this->queue->receive();
try {
$job->process();
} finally {
$job->markDone();
}
}
});
}
public function stop(): void
{
// Todas las corrutinas seran detenidas aqui
$this->scope->cancel();
}
}
Que Sigue?
- Scope – gestion de grupos de corrutinas
- Corrutinas – ciclo de vida de las corrutinas
- Canales – intercambio de datos entre corrutinas