Guarda un punto de control, y luego recupera
Publicar mantiene informado al usuario. Guardar puntos de control es lo que salva el trabajo cuando algo va mal:
// Save a checkpoint so failed() can recover partial state if the job is killed
Cache::put($this->checkpointKey(), array_merge($state, ['conversationHistory' => $history]), now()->addMinutes(5));
Y luego la parte que la mayoría deja vacía:
public function failed(\Throwable $exception): void
{
report($exception);
$checkpoint = Cache::pull($this->checkpointKey());
$base = $checkpoint ?? [ /* the state the job started with */ ];
$base['messages'][] = [
'role' => 'bot',
'content' => __('messages.errors.ai_error'),
'is_error' => true,
];
$base['hasError'] = true;
$base['done'] = true;
Cache::forget(self::cancelKey($this->cacheKey));
Cache::put($this->cacheKey, $base, now()->addMinutes(5));
}
Tres cosas se ganan su sitio aquí.
Recupera estado parcial. Dos resultados de herramientas útiles y luego un fallo no debería descartar los dos resultados. Cache::pull lee y borra en un solo paso.
Fija done. El camino de fallo debe detener la consulta del cliente. Una función que falla pero nunca lo dice es peor que una que falla ruidosamente: el usuario espera indefinidamente algo que no va a llegar.
Limpia sus propias claves. Se eliminan banderas de cancelación y puntos de control, para que el siguiente turno arranque de cero en vez de heredar una instrucción rancia.
failed() no es manejo de errores atornillado a un lado. Es la segunda implementación de tu función —la que se ejecuta el peor día— y merece la misma atención que handle().
Deja parar al usuario
Que algo dure mucho significa que un usuario querrá interrumpirlo. Una vez el job está corriendo no queda nada que cancelar salvo las idas y vueltas que aún no ha hecho, así que la cancelación es cooperativa:
// The visitor pressed stop. Checked before the call rather than after, so an
// in-flight round-trip is the most that can still be paid for; whatever has
// already landed stays on screen.
if (Cache::has(self::cancelKey($this->cacheKey))) {
$state['stopped'] = true;
break;
}
Dos detalles que copiar.
Se comprueba arriba del bucle, no abajo. Comprueba después de la llamada y siempre pagarás una ida y vuelta más de la que el usuario pidió. Arriba, una petición en vuelo es el desperdicio máximo.
Una bandera en caché, no una señal a la cola. El job ya está corriendo en algún worker; no hay canal hacia él. Una clave que el frontend fija y el job lee es lo más simple que funciona, y parar no es urgente al milisegundo.
Fíjate además en que parar no es fallar. stopped es su propio estado: nada de lo que hay en pantalla se descarta, no se muestra ningún error. El usuario pidió que parara y paró.