use std::sync::{Arc, Mutex};
use tokio::sync::broadcast;
use uuid::Uuid;
use crate::db::entities::Estado;
pub const CAPACIDAD_DEL_HUB: usize = 256;
#[derive(Debug, Clone, PartialEq)]
pub struct EventoDeEjecucion {
pub execution_id: Uuid,
pub job_id: Uuid,
pub status: Estado,
pub bytes_processed: u64,
pub bytes_uploaded: u64,
pub percentage: Option<f64>,
pub current_action: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct EventoDeRestauracion {
pub execution_id: Uuid,
pub snapshot_id: String,
pub phase: String,
pub files_done: u64,
pub files_total: u64,
pub bytes_done: u64,
pub bytes_total: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum EventoDelMotor {
Ejecucion(EventoDeEjecucion),
Restauracion(EventoDeRestauracion),
}
#[derive(Clone)]
pub struct Hub {
emisor: Arc<Mutex<Option<broadcast::Sender<EventoDelMotor>>>>,
}
impl Hub {
pub fn nuevo() -> Self {
let (emisor, _) = broadcast::channel(CAPACIDAD_DEL_HUB);
Self {
emisor: Arc::new(Mutex::new(Some(emisor))),
}
}
pub fn suscribir(&self) -> Option<broadcast::Receiver<EventoDelMotor>> {
let emisor = self
.emisor
.lock()
.unwrap_or_else(|envenenado| envenenado.into_inner());
emisor.as_ref().map(broadcast::Sender::subscribe)
}
pub fn publicar(&self, evento: EventoDeEjecucion) {
self.enviar(EventoDelMotor::Ejecucion(evento));
}
pub fn publicar_restauracion(&self, evento: EventoDeRestauracion) {
self.enviar(EventoDelMotor::Restauracion(evento));
}
fn enviar(&self, evento: EventoDelMotor) {
let emisor = self
.emisor
.lock()
.unwrap_or_else(|envenenado| envenenado.into_inner());
if let Some(emisor) = emisor.as_ref() {
let _ = emisor.send(evento);
}
}
pub fn cerrar(&self) {
let mut emisor = self
.emisor
.lock()
.unwrap_or_else(|envenenado| envenenado.into_inner());
let _ = emisor.take();
}
pub fn capacidad(&self) -> usize {
CAPACIDAD_DEL_HUB
}
}
impl Default for Hub {
fn default() -> Self {
Self::nuevo()
}
}
impl std::fmt::Debug for Hub {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Hub")
.field("capacidad", &self.capacidad())
.finish()
}
}
#[cfg(test)]
mod tests {
use tokio::sync::broadcast::error::{RecvError, TryRecvError};
use uuid::Uuid;
use super::{EventoDeEjecucion, EventoDelMotor, Hub};
use crate::db::entities::Estado;
fn evento(accion: &str) -> EventoDeEjecucion {
EventoDeEjecucion {
execution_id: Uuid::new_v4(),
job_id: Uuid::new_v4(),
status: Estado::Running,
bytes_processed: 1024,
bytes_uploaded: 512,
percentage: None,
current_action: accion.to_owned(),
}
}
#[test]
fn dos_suscriptores_reciben_el_mismo_evento() {
let hub = Hub::nuevo();
hub.publicar(evento("antes de que nadie escuche"));
let mut uno = hub
.suscribir()
.expect("el hub no está cerrado en esta prueba");
let mut otro = hub
.suscribir()
.expect("el hub no está cerrado en esta prueba");
let publicado = evento("archivando");
hub.publicar(publicado.clone());
let del_primero = uno
.try_recv()
.expect("el primer suscriptor tiene que recibir el evento");
let del_segundo = otro
.try_recv()
.expect("el segundo suscriptor tiene que recibir el mismo evento");
assert_eq!(
del_primero,
EventoDelMotor::Ejecucion(publicado.clone()),
"el primer suscriptor recibió un evento que no es el publicado"
);
assert_eq!(
del_segundo,
EventoDelMotor::Ejecucion(publicado),
"los dos suscriptores no recibieron el mismo evento"
);
}
#[test]
fn el_hub_no_crece_sin_tope_y_el_cliente_lento_no_frena_al_motor() {
let hub = Hub::nuevo();
let capacidad = hub.capacidad();
let mut lento = hub
.suscribir()
.expect("el hub no está cerrado en esta prueba");
let empezado = std::time::Instant::now();
for publicado in 0..(capacidad + 50) {
hub.publicar(evento(&format!("aviso {publicado}")));
}
let tardado = empezado.elapsed();
let mut entregados = 0usize;
let mut perdidos = 0usize;
loop {
match lento.try_recv() {
Ok(_) => entregados += 1,
Err(TryRecvError::Lagged(cuantos)) => {
perdidos = perdidos.max(cuantos as usize);
}
Err(TryRecvError::Empty | TryRecvError::Closed) => break,
}
}
assert!(
perdidos > 0,
"un cliente que no lee no perdió ni un evento con {capacidad} de capacidad y \
{} publicados: el hub no tiene tope",
capacidad + 50
);
assert!(
entregados <= capacidad,
"el receptor entregó {entregados} eventos con {capacidad} de capacidad: el hub creció"
);
assert!(
entregados > 0,
"el receptor no recibió nada: el reparto está roto y la medición no dice nada"
);
assert!(
tardado < std::time::Duration::from_secs(5),
"publicar {} eventos tardó {tardado:?}: el hub está esperando a alguien, y eso es \
un cliente que frena a las copias",
capacidad + 50
);
}
#[test]
fn dos_avisos_seguidos_llegan_los_dos_y_en_orden() {
let hub = Hub::nuevo();
let mut cliente = hub
.suscribir()
.expect("el hub no está cerrado en esta prueba");
hub.publicar(evento("extrayendo el volcado"));
hub.publicar(evento("archivando"));
let primero = cliente
.try_recv()
.expect("el primer aviso tiene que llegar");
let segundo = cliente
.try_recv()
.expect("el segundo aviso tiene que llegar");
let EventoDelMotor::Ejecucion(primero) = primero else {
panic!("el hub repartió un aviso de restauración donde se esperaba una ejecución");
};
let EventoDelMotor::Ejecucion(segundo) = segundo else {
panic!("el hub repartió un aviso de restauración donde se esperaba una ejecución");
};
assert_eq!(primero.current_action, "extrayendo el volcado");
assert_eq!(
segundo.current_action, "archivando",
"los avisos llegaron desordenados: el panel pintaría una acción vieja como la de ahora"
);
}
#[test]
fn publicar_sin_suscriptores_vivos_no_falla() {
let hub = Hub::nuevo();
{
let cliente = hub
.suscribir()
.expect("el hub no está cerrado en esta prueba");
drop(cliente);
}
hub.publicar(evento("sin nadie escuchando"));
let mut tardio = hub
.suscribir()
.expect("el hub no está cerrado en esta prueba");
hub.publicar(evento("el que llega tarde"));
assert!(
tardio.try_recv().is_ok(),
"el hub dejó de repartir después de quedarse sin suscriptores"
);
}
#[tokio::test]
async fn un_receptor_sin_nada_que_leer_espera() {
let hub = Hub::nuevo();
let mut cliente = hub
.suscribir()
.expect("el hub no está cerrado en esta prueba");
assert!(
tokio::time::timeout(std::time::Duration::from_millis(50), cliente.recv())
.await
.is_err(),
"un receptor sin eventos devolvió algo, cuando lo que tiene que hacer es esperar"
);
assert!(
!matches!(cliente.try_recv(), Err(TryRecvError::Closed)),
"el canal se cerró por una espera: el hub no puede morir porque nadie publique"
);
hub.publicar(evento("después de la espera"));
assert!(
cliente.recv().await.is_ok(),
"el aviso no llegó después de la espera"
);
assert!(matches!(cliente.try_recv(), Err(TryRecvError::Empty)));
}
#[tokio::test]
async fn el_cierre_despierta_al_cliente_que_esperaba_y_no_deja_suscribir_a_nadie_mas() {
let hub = Hub::nuevo();
let mut cliente = hub
.suscribir()
.expect("el hub no está cerrado en esta prueba");
hub.cerrar();
let desenlace =
tokio::time::timeout(std::time::Duration::from_secs(5), cliente.recv()).await;
assert!(
matches!(desenlace, Ok(Err(RecvError::Closed))),
"el cierre no despertó al cliente con su `Closed`: el cuerpo del stream no terminaría y \
el apagado ordenado se quedaría esperándolo ({desenlace:?})"
);
assert!(
hub.suscribir().is_none(),
"un hub cerrado sigue dando receptores: una petición que llegue durante el apagado \
tendría un stream abierto que ya no puede recibir nada"
);
}
#[test]
fn cerrar_es_idempotente_y_publicar_despues_no_falla() {
let hub = Hub::nuevo();
let mut receptor = hub
.suscribir()
.expect("el hub no está cerrado en esta prueba");
hub.cerrar();
hub.cerrar();
hub.publicar(evento("después del cierre"));
assert!(matches!(receptor.try_recv(), Err(TryRecvError::Closed)));
}
#[tokio::test]
async fn el_cierre_alcanza_a_los_clones_del_hub() {
let hub = Hub::nuevo();
let en_vivo = hub.clone();
let mut cliente = hub
.suscribir()
.expect("el hub no está cerrado en esta prueba");
hub.cerrar();
en_vivo.publicar(evento("el aviso de una copia en vuelo"));
let desenlace =
tokio::time::timeout(std::time::Duration::from_secs(5), cliente.recv()).await;
assert!(
matches!(desenlace, Ok(Err(RecvError::Closed))),
"un clon del hub mantuvo el canal abierto después del cierre: es exactamente el caso de \
`EnVivo`, y con él el apagado se cuelga aunque el motor cierre su hub ({desenlace:?})"
);
}
}